tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from datetime import datetime, timedelta
  41from dateutil.tz import tzlocal, tzutc
  42from time import sleep
  43
  44import re
  45import json
  46import requests
  47import traceback as tb
  48from typing import Union
  49
  50from multiprocessing import cpu_count
  51from multiprocessing.pool import ThreadPool
  52import pandas as pd
  53
  54from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  55
  56from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  57from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  58
  59import UniLogger as uLog  # Logger for TKSBrokerAPI
  60
  61
  62# --- Common technical parameters:
  63
  64PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  65uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  66uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  67uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  68
  69__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  70
  71CPU_COUNT = cpu_count()  # host's real CPU count
  72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  73
  74# --- Main constants:
  75
  76NANO = 0.000000001  # SI-constant nano = 10^-9
  77
  78
  79def NanoToFloat(units: str, nano: int) -> float:
  80    """
  81    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
  82
  83    `NanoToFloat(units="2", nano=500000000) -> 2.5`
  84
  85    `NanoToFloat(units="0", nano=50000000) -> 0.05`
  86
  87    :param units: integer string or integer parameter that represents the integer part of number
  88    :param nano: integer string or integer parameter that represents the fractional part of number
  89    :return: float view of number
  90    """
  91    return int(units) + int(nano) * NANO
  92
  93
  94def FloatToNano(number: float) -> dict:
  95    """
  96    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
  97
  98    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
  99
 100    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
 101
 102    :param number: float number
 103    :return: nano-type view of number: `{"units": "string", "nano": integer}`
 104    """
 105    splitByPoint = str(number).split(".")
 106    frac = 0
 107
 108    if len(splitByPoint) > 1:
 109        if len(splitByPoint[1]) <= 9:
 110            frac = int("{}{}".format(
 111                int(splitByPoint[1]),
 112                "0" * (9 - len(splitByPoint[1])),
 113            ))
 114
 115    if (number < 0) and (frac > 0):
 116        frac = -frac
 117
 118    return {"units": str(int(number)), "nano": frac}
 119
 120
 121def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 122    """
 123    Create tuple of date and time strings with timezone parsed from user-friendly date.
 124
 125    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 126
 127    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 128    An error exception will occur if input date has incorrect format.
 129
 130    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 131    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 132    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 133    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
 134
 135    Also, you can use keywords for start if `end=None`:
 136    `today` (from 00:00:00 to the end of current day),
 137    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 138    `week` (-7 day from 00:00:00 to the end of current day),
 139    `month` (-30 day from 00:00:00 to the end of current day),
 140    `year` (-365 day from 00:00:00 to the end of current day),
 141
 142    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 143             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
 144             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 145    """
 146    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 147    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 148    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 149
 150    # time between start and the end of the current day:
 151    if start is None or start.lower() == "today":
 152        pass
 153
 154    # from start of the last day to the end of the last day:
 155    elif start.lower() == "yesterday":
 156        s -= timedelta(days=1)
 157        e -= timedelta(days=1)
 158
 159    # week (-7 day from 00:00:00 to the end of the current day):
 160    elif start.lower() == "week":
 161        s -= timedelta(days=6)  # +1 current day already taken into account
 162
 163    # month (-30 day from 00:00:00 to the end of current day):
 164    elif start.lower() == "month":
 165        s -= timedelta(days=29)  # +1 current day already taken into account
 166
 167    # year (-365 day from 00:00:00 to the end of current day):
 168    elif start.lower() == "year":
 169        s -= timedelta(days=364)  # +1 current day already taken into account
 170
 171    # -N days ago to the end of current day:
 172    elif start.startswith('-') and start[1:].isdigit():
 173        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 174
 175    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 176    else:
 177        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 178        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 179
 180    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 181    s = s.strftime(TKS_DATE_TIME_FORMAT)
 182    e = e.strftime(TKS_DATE_TIME_FORMAT)
 183
 184    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 185
 186    return s, e
 187
 188
 189class TinkoffBrokerServer:
 190    """
 191    This class implements methods to work with Tinkoff broker server.
 192
 193    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 194
 195    About `token`: https://tinkoff.github.io/investAPI/token/
 196    """
 197    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 198        """
 199        Main class init.
 200
 201        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 202        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 203                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 204        :param useCache: use default cache file with raw data to use instead of `iList`.
 205                         True by default. Cache is auto-update if new day has come.
 206                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 207        :param defaultCache: path to default cache file. `dump.json` by default.
 208        """
 209        if token is None or not token:
 210            try:
 211                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 212                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 213
 214            except KeyError:
 215                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 216                raise Exception("Token required")
 217
 218        else:
 219            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 220            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 221
 222        if accountId is None or not accountId:
 223            try:
 224                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 225                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 226
 227            except KeyError:
 228                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 229
 230        else:
 231            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 232            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 233
 234        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 235        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 236
 237        Latest version: https://pypi.org/project/tksbrokerapi/
 238        """
 239
 240        self.aliases = TKS_TICKER_ALIASES
 241        """Some aliases instead official tickers.
 242
 243        See also: `TKSEnums.TKS_TICKER_ALIASES`
 244        """
 245
 246        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 247
 248        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 249
 250        self.ticker = ""
 251        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 252
 253        See also: `SearchByTicker()`, `SearchInstruments()`.
 254        """
 255
 256        self.figi = ""
 257        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 258
 259        See also: `SearchByFIGI()`, `SearchInstruments()`.
 260        """
 261
 262        self.depth = 1
 263        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 264
 265        See also: `GetCurrentPrices()`.
 266        """
 267
 268        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 269        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 270
 271        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 272        """
 273
 274        uLogger.debug("Broker API server: {}".format(self.server))
 275
 276        self.timeout = 15
 277        """Server operations timeout in seconds. Default: `15`.
 278
 279        See also: `SendAPIRequest()`.
 280        """
 281
 282        self.headers = {
 283            "Content-Type": "application/json",
 284            "accept": "application/json",
 285            "Authorization": "Bearer {}".format(self.token),
 286            "x-app-name": "Tim55667757.TKSBrokerAPI",
 287        }
 288        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 289
 290        See also: `SendAPIRequest()`.
 291        """
 292
 293        self.body = None
 294        """Request body which send to broker server. Default: `None`.
 295
 296        See also: `SendAPIRequest()`.
 297        """
 298
 299        self.moreDebug = False
 300        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 301
 302        self.historyFile = None
 303        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 304
 305        See also: `History()`.
 306        """
 307
 308        self.htmlHistoryFile = "index.html"
 309        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 310
 311        See also: `ShowHistoryChart()`.
 312        """
 313
 314        self.instrumentsFile = "instruments.md"
 315        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 316
 317        See also: `ShowInstrumentsInfo()`.
 318        """
 319
 320        self.searchResultsFile = "search-results.md"
 321        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 322
 323        See also: `SearchInstruments()`.
 324        """
 325
 326        self.pricesFile = "prices.md"
 327        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 328
 329        See also: `GetListOfPrices()`.
 330        """
 331
 332        self.infoFile = "info.md"
 333        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 334
 335        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 336        """
 337
 338        self.bondsXLSXFile = "ext-bonds.xlsx"
 339        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 340        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 341
 342        See also: `ExtendBondsData()`.
 343        """
 344
 345        self.calendarFile = "calendar.md"
 346        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 347        
 348        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 349
 350        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 351        """
 352
 353        self.overviewFile = "overview.md"
 354        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 355
 356        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 357        """
 358
 359        self.overviewDigestFile = "overview-digest.md"
 360        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 361
 362        See also: `Overview()` with parameter `details="digest"`.
 363        """
 364
 365        self.overviewPositionsFile = "overview-positions.md"
 366        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 367
 368        See also: `Overview()` with parameter `details="positions"`.
 369        """
 370
 371        self.overviewOrdersFile = "overview-orders.md"
 372        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 373
 374        See also: `Overview()` with parameter `details="orders"`.
 375        """
 376
 377        self.overviewAnalyticsFile = "overview-analytics.md"
 378        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 379
 380        See also: `Overview()` with parameter `details="analytics"`.
 381        """
 382
 383        self.reportFile = "deals.md"
 384        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 385
 386        See also: `Deals()`.
 387        """
 388
 389        self.withdrawalLimitsFile = "limits.md"
 390        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 391
 392        See also: `OverviewLimits()` and `RequestLimits()`.
 393        """
 394
 395        self.userInfoFile = "user-info.md"
 396        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 397
 398        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 399        """
 400
 401        self.userAccountsFile = "accounts.md"
 402        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 403
 404        See also: `OverviewAccounts()`, `RequestAccounts()`.
 405        """
 406
 407        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 408        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 409
 410        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 411
 412        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 413        """
 414
 415        self.iList = None  # init iList for raw instruments data
 416        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 417        
 418        See also: `Listing()`, `DumpInstruments()`.
 419        """
 420
 421        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 422        if useCache:
 423            if os.path.exists(self.iListDumpFile):
 424                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 425                curTime = datetime.now(tzutc())
 426
 427                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 428                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 429
 430                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 431
 432                else:
 433                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 434
 435                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 436                        os.path.abspath(self.iListDumpFile),
 437                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 438                    ))
 439
 440            else:
 441                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 442                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 443
 444        else:
 445            self.iList = self.Listing()  # request new raw instruments data from broker server
 446            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 447
 448        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 449        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 450
 451        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 452        """
 453
 454    def _ParseJSON(self, rawData="{}") -> dict:
 455        """
 456        Parse JSON from response string.
 457
 458        :param rawData: this is a string with JSON-formatted text.
 459        :return: JSON (dictionary), parsed from server response string.
 460        """
 461        responseJSON = json.loads(rawData) if rawData else {}
 462
 463        if self.moreDebug:
 464            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 465
 466        return responseJSON
 467
 468    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 469        """
 470        Send GET or POST request to broker server and receive JSON object.
 471
 472        self.header: must be defining with dictionary of headers.
 473        self.body: if define then used as request body. None by default.
 474        self.timeout: global request timeout, 15 seconds by default.
 475        :param url: url with REST request.
 476        :param reqType: send "GET" or "POST" request. "GET" by default.
 477        :param retry: how many times retry after first request if an 5xx server errors occurred.
 478        :param pause: sleep time in seconds between retries.
 479        :return: response JSON (dictionary) from broker.
 480        """
 481        if reqType not in ("GET", "POST"):
 482            uLogger.error("You can define request type: 'GET' or 'POST'!")
 483            raise Exception("Incorrect value")
 484
 485        if self.moreDebug:
 486            uLogger.debug("Request parameters:")
 487            uLogger.debug("    - REST API URL: {}".format(url))
 488            uLogger.debug("    - request type: {}".format(reqType))
 489            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 490            uLogger.debug("    - body:\n{}".format(self.body))
 491
 492        # fast hack to avoid all operations with some tickers/FIGI
 493        responseJSON = {}
 494        oK = True
 495        for item in self.exclude:
 496            if item in url:
 497                if self.moreDebug:
 498                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 499
 500                oK = False
 501                break
 502
 503        if oK:
 504            counter = 0
 505            response = None
 506            errMsg = ""
 507
 508            while not response and counter <= retry:
 509                if reqType == "GET":
 510                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 511
 512                if reqType == "POST":
 513                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 514
 515                if self.moreDebug:
 516                    uLogger.debug("Response:")
 517                    uLogger.debug("    - status code: {}".format(response.status_code))
 518                    uLogger.debug("    - reason: {}".format(response.reason))
 519                    uLogger.debug("    - body length: {}".format(len(response.text)))
 520                    uLogger.debug("    - headers:\n{}".format(response.headers))
 521
 522                # Server returns some headers:
 523                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 524                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 525                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 526                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 527                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 528                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 529                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 530                    sleep(rateLimitWait)
 531
 532                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 533                if 400 <= response.status_code < 500:
 534                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 535                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 536                    counter = retry + 1
 537
 538                if 500 <= response.status_code < 600:
 539                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 540                    uLogger.debug("    - not oK, {}".format(errMsg))
 541                    counter += 1
 542
 543                    if counter <= retry:
 544                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 545                        sleep(pause)
 546
 547            responseJSON = self._ParseJSON(rawData=response.text)
 548
 549            if errMsg:
 550                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 551                uLogger.error("    - not oK, {}".format(errMsg))
 552
 553        return responseJSON
 554
 555    def _IUpdater(self, iType: str) -> tuple:
 556        """
 557        Request instrument by type from server. See available API methods for instruments:
 558        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 559        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 560        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 561        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 562        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 563
 564        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 565        :return: tuple with iType name and list of available instruments of current type for defined user token.
 566        """
 567        result = []
 568
 569        if iType in TKS_INSTRUMENTS:
 570            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 571
 572            # all instruments have the same body in API v2 requests:
 573            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 574            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 575            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 576
 577        return iType, result
 578
 579    def _IWrapper(self, kwargs):
 580        """
 581        Wrapper runs instrument's update method `_IUpdater()`.
 582        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 583        """
 584        return self._IUpdater(**kwargs)
 585
 586    def Listing(self) -> dict:
 587        """
 588        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 589
 590        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 591        """
 592        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 593        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 594
 595        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 596        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 597        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 598
 599        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 600        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 601        poolUpdater.close()
 602
 603        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 604        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 605        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 606
 607        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 608        for iType in iList.keys():
 609            for ticker in iList[iType]:
 610                iList[iType][ticker]["type"] = iType
 611
 612                if "minPriceIncrement" in iList[iType][ticker].keys():
 613                    iList[iType][ticker]["step"] = NanoToFloat(
 614                        iList[iType][ticker]["minPriceIncrement"]["units"],
 615                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 616                    )
 617
 618                else:
 619                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 620
 621        return iList
 622
 623    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 624        """
 625        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 626
 627        See also: `DumpInstruments()`, `Listing()`.
 628
 629        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 630                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 631        """
 632        if self.iListDumpFile is None or not self.iListDumpFile:
 633            uLogger.error("Output name of dump file must be defined!")
 634            raise Exception("Filename required")
 635
 636        if not self.iList or forceUpdate:
 637            self.iList = self.Listing()
 638
 639        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 640
 641        # Save as XLSX with separated sheets for every type of instruments:
 642        with pd.ExcelWriter(
 643                path=xlsxDumpFile,
 644                date_format=TKS_DATE_FORMAT,
 645                datetime_format=TKS_DATE_TIME_FORMAT,
 646                mode="w",
 647        ) as writer:
 648            for iType in TKS_INSTRUMENTS:
 649                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 650                df = df[sorted(df)]  # sorted by column names
 651                df = df.applymap(
 652                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 653                    na_action="ignore",
 654                )  # converting numbers from nano-type to float in every cell
 655                df.to_excel(
 656                    writer,
 657                    sheet_name=iType,
 658                    encoding="UTF-8",
 659                    freeze_panes=(1, 1),
 660                )  # saving as XLSX-file with freeze first row and column as headers
 661
 662        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 663
 664    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 665        """
 666        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 667        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 668
 669        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 670
 671        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 672                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 673        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 674        """
 675        if self.iListDumpFile is None or not self.iListDumpFile:
 676            uLogger.error("Output name of dump file must be defined!")
 677            raise Exception("Filename required")
 678
 679        if not self.iList or forceUpdate:
 680            self.iList = self.Listing()
 681
 682        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 683        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 684            fH.write(jsonDump)
 685
 686        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 687
 688        return jsonDump
 689
 690    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 691        """
 692        Show information about one instrument defined by json data and prints it in Markdown format.
 693
 694        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 695
 696        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 697        :param show: if `True` then also printing information about instrument and its current price.
 698        :return: multilines text in Markdown format with information about one instrument.
 699        """
 700        splitLine = "|                                                             |                                                        |\n"
 701        infoText = ""
 702
 703        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 704            info = [
 705                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 706                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 707                "| Parameters                                                  | Values                                                 |\n",
 708                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 709                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 710                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 711            ]
 712
 713            if "sector" in iJSON.keys() and iJSON["sector"]:
 714                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 715
 716            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 717                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 718                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 719            )))
 720
 721            info.extend([
 722                splitLine,
 723                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 724                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 725            ])
 726
 727            if "isin" in iJSON.keys() and iJSON["isin"]:
 728                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 729
 730            if "classCode" in iJSON.keys():
 731                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 732
 733            info.extend([
 734                splitLine,
 735                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 736                splitLine,
 737                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 738                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 739                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 740            ])
 741
 742            if iJSON["figi"]:
 743                self.figi = iJSON["figi"]
 744                iJSON = iJSON | self.RequestTradingStatus()
 745
 746                info.extend([
 747                    splitLine,
 748                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 749                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 750                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 751                ])
 752
 753            info.append(splitLine)
 754
 755            if "type" in iJSON.keys() and iJSON["type"]:
 756                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 757
 758            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 759                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 760
 761            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 762                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 763
 764            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 765                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 766
 767            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 768                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 769
 770            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 771                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 772
 773            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 774                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 775
 776            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 777                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 778
 779            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 780                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 781
 782            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 783                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 784
 785            if "currency" in iJSON.keys():
 786                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 787
 788            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 789                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 790
 791            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 792                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 793
 794            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 795                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 796
 797            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 798                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 799
 800            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 801                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 802
 803            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 804                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 805
 806            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 807                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 808
 809            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 810                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 811
 812            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 813                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 814
 815            iExt = None
 816            if iJSON["type"] == "Bonds":
 817                info.extend([
 818                    splitLine,
 819                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 820                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 821                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 822                        iJSON["nominal"]["currency"],
 823                    )),
 824                ])
 825
 826                if "floatingCouponFlag" in iJSON.keys():
 827                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 828
 829                if "amortizationFlag" in iJSON.keys():
 830                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 831
 832                info.append(splitLine)
 833
 834                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 835                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 836
 837                if iJSON["figi"]:
 838                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 839
 840                    info.extend([
 841                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 842                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 843                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 844                    ])
 845
 846                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 847                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 848                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 849                        iJSON["aciValue"]["currency"]
 850                    )))
 851
 852            if "currentPrice" in iJSON.keys():
 853                info.append(splitLine)
 854
 855                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 856                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 857
 858                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 859                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 860                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 861                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 862                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 863
 864                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 865                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 866
 867                info.extend([
 868                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 869                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 870                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 871                    )),
 872                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 873                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 874                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 875                    )),
 876                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 877                        "{:.2f}%{}".format(
 878                            iJSON["currentPrice"]["changes"],
 879                            " ({}{:.2f} {})".format(
 880                                "+" if bondChangesDelta > 0 else "",
 881                                bondChangesDelta,
 882                                aciCurrency
 883                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 884                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 885                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 886                                currency
 887                            ),
 888                        )
 889                    ),
 890                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 891                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 892                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 893                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 894                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 895                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 896                    )),
 897                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 898                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 899                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 900                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 901                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 902                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 903                    )),
 904                ])
 905
 906            if "lot" in iJSON.keys():
 907                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 908
 909            if "step" in iJSON.keys() and iJSON["step"] != 0:
 910                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 911
 912            # Add bond payment calendar:
 913            if iJSON["type"] == "Bonds":
 914                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 915                info.extend(["\n", strCalendar])
 916
 917            infoText += "".join(info)
 918
 919            if show:
 920                uLogger.info("{}".format(infoText))
 921
 922            else:
 923                uLogger.debug("{}".format(infoText))
 924
 925            if self.infoFile is not None:
 926                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 927                    fH.write(infoText)
 928
 929                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 930
 931        return infoText
 932
 933    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 934        """
 935        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 936
 937        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 938        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 939        :return: JSON formatted data with information about instrument.
 940        """
 941        tickerJSON = {}
 942        if self.moreDebug:
 943            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 944
 945        if not self.ticker:
 946            uLogger.warning("self.ticker variable is not be empty!")
 947
 948        else:
 949            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 950                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 951                raise Exception("Instrument not allowed")
 952
 953            if not self.iList:
 954                self.iList = self.Listing()
 955
 956            if self.ticker in self.iList["Shares"].keys():
 957                tickerJSON = self.iList["Shares"][self.ticker]
 958                if self.moreDebug:
 959                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 960
 961            elif self.ticker in self.iList["Currencies"].keys():
 962                tickerJSON = self.iList["Currencies"][self.ticker]
 963                if self.moreDebug:
 964                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 965
 966            elif self.ticker in self.iList["Bonds"].keys():
 967                tickerJSON = self.iList["Bonds"][self.ticker]
 968                if self.moreDebug:
 969                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 970
 971            elif self.ticker in self.iList["Etfs"].keys():
 972                tickerJSON = self.iList["Etfs"][self.ticker]
 973                if self.moreDebug:
 974                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 975
 976            elif self.ticker in self.iList["Futures"].keys():
 977                tickerJSON = self.iList["Futures"][self.ticker]
 978                if self.moreDebug:
 979                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 980
 981        if tickerJSON:
 982            self.figi = tickerJSON["figi"]
 983
 984            if requestPrice:
 985                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 986
 987                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 988                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 989
 990                else:
 991                    tickerJSON["currentPrice"]["changes"] = 0
 992
 993            if show:
 994                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 995
 996        else:
 997            if show:
 998                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 999
1000        return tickerJSON
1001
1002    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1003        """
1004        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1005
1006        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1007        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1008        :return: JSON formatted data with information about instrument.
1009        """
1010        figiJSON = {}
1011        if self.moreDebug:
1012            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1013
1014        if not self.figi:
1015            uLogger.warning("self.figi variable is not be empty!")
1016
1017        else:
1018            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1019                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1020                raise Exception("Instrument not allowed")
1021
1022            if not self.iList:
1023                self.iList = self.Listing()
1024
1025            for item in self.iList["Shares"].keys():
1026                if self.figi == self.iList["Shares"][item]["figi"]:
1027                    figiJSON = self.iList["Shares"][item]
1028
1029                    if self.moreDebug:
1030                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1031
1032                    break
1033
1034            if not figiJSON:
1035                for item in self.iList["Currencies"].keys():
1036                    if self.figi == self.iList["Currencies"][item]["figi"]:
1037                        figiJSON = self.iList["Currencies"][item]
1038
1039                        if self.moreDebug:
1040                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1041
1042                        break
1043
1044            if not figiJSON:
1045                for item in self.iList["Bonds"].keys():
1046                    if self.figi == self.iList["Bonds"][item]["figi"]:
1047                        figiJSON = self.iList["Bonds"][item]
1048
1049                        if self.moreDebug:
1050                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1051
1052                        break
1053
1054            if not figiJSON:
1055                for item in self.iList["Etfs"].keys():
1056                    if self.figi == self.iList["Etfs"][item]["figi"]:
1057                        figiJSON = self.iList["Etfs"][item]
1058
1059                        if self.moreDebug:
1060                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1061
1062                        break
1063
1064            if not figiJSON:
1065                for item in self.iList["Futures"].keys():
1066                    if self.figi == self.iList["Futures"][item]["figi"]:
1067                        figiJSON = self.iList["Futures"][item]
1068
1069                        if self.moreDebug:
1070                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1071
1072                        break
1073
1074        if figiJSON:
1075            self.figi = figiJSON["figi"]
1076            self.ticker = figiJSON["ticker"]
1077
1078            if requestPrice:
1079                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1080
1081                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1082                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1083
1084                else:
1085                    figiJSON["currentPrice"]["changes"] = 0
1086
1087            if show:
1088                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1089
1090        else:
1091            if show:
1092                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1093
1094        return figiJSON
1095
1096    def GetCurrentPrices(self, show: bool = True) -> dict:
1097        """
1098        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1099        `{"buy": [{"price": 1243.8, "quantity": 193},
1100                  {"price": 1244.0, "quantity": 168},
1101                  {"price": 1244.8, "quantity": 5},
1102                  {"price": 1245.0, "quantity": 61},
1103                  {"price": 1245.4, "quantity": 60}],
1104          "sell": [{"price": 1243.6, "quantity": 8},
1105                   {"price": 1242.6, "quantity": 10},
1106                   {"price": 1242.4, "quantity": 18},
1107                   {"price": 1242.2, "quantity": 50},
1108                   {"price": 1242.0, "quantity": 113}],
1109          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1110        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1111        - sell: list of dicts with Buyers prices,
1112            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1113            - quantity: volume value by current price in lots,
1114        - limitUp: current trade session limit price, maximum,
1115        - limitDown: current trade session limit price, minimum,
1116        - lastPrice: last deal price of the instrument,
1117        - closePrice: previous trade session close price of the instrument.
1118
1119        See also: `SearchByTicker()` and `SearchByFIGI()`.
1120        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1121        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1122
1123        :param show: if `True` then print DOM to log and console.
1124        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1125                 If an error occurred then returns an empty record:
1126                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1127        """
1128        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1129
1130        if self.depth < 1:
1131            uLogger.error("Depth of Market (DOM) must be >=1!")
1132            raise Exception("Incorrect value")
1133
1134        if not (self.ticker or self.figi):
1135            uLogger.error("self.ticker or self.figi variables must be defined!")
1136            raise Exception("Ticker or FIGI required")
1137
1138        if self.ticker and not self.figi:
1139            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1140            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1141
1142        if not self.ticker and self.figi:
1143            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1144            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1145
1146        if not self.figi:
1147            uLogger.error("FIGI is not defined!")
1148            raise Exception("Ticker or FIGI required")
1149
1150        else:
1151            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1152
1153            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1154            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1155            self.body = str({"figi": self.figi, "depth": self.depth})
1156            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1157
1158            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1159                # list of dicts with sellers orders:
1160                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1161
1162                # list of dicts with buyers orders:
1163                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1164
1165                # max price of instrument at this time:
1166                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1167
1168                # min price of instrument at this time:
1169                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1170
1171                # last price of deal with instrument:
1172                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1173
1174                # last close price of instrument:
1175                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1176
1177            else:
1178                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1179                uLogger.debug("Server response: {}".format(pricesResponse))
1180
1181            if show:
1182                if prices["buy"] or prices["sell"]:
1183                    info = [
1184                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1185                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1186                            self.ticker,
1187                            self.figi,
1188                            self.depth,
1189                        ),
1190                        "-" * 60, "\n",
1191                        "             Orders of Buyers | Orders of Sellers\n",
1192                        "-" * 60, "\n",
1193                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1194                        "-" * 60, "\n",
1195                    ]
1196
1197                    if not prices["buy"]:
1198                        info.append("                              | No orders!\n")
1199                        sumBuy = 0
1200
1201                    else:
1202                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1203                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1204                        for item in maxMinSorted:
1205                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1206
1207                    if not prices["sell"]:
1208                        info.append("No orders!                    |\n")
1209                        sumSell = 0
1210
1211                    else:
1212                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1213                        for item in prices["sell"]:
1214                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1215
1216                    info.extend([
1217                        "-" * 60, "\n",
1218                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1219                        "-" * 60, "\n",
1220                    ])
1221
1222                    infoText = "".join(info)
1223
1224                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1225
1226                else:
1227                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1228
1229        return prices
1230
1231    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1232        """
1233        This method get and show information about all available broker instruments for current user account.
1234        If `instrumentsFile` string is not empty then also save information to this file.
1235
1236        :param show: if `True` then print results to console, if `False` - print only to file.
1237        :return: multi-lines string with all available broker instruments
1238        """
1239        if not self.iList:
1240            self.iList = self.Listing()
1241
1242        info = [
1243            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1244            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1245        ]
1246
1247        # add instruments count by type:
1248        for iType in self.iList.keys():
1249            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1250
1251        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1252        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1253
1254        # generating info tables with all instruments by type:
1255        for iType in self.iList.keys():
1256            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1257
1258            for instrument in self.iList[iType].keys():
1259                iName = self.iList[iType][instrument]["name"]  # instrument's name
1260                if len(iName) > 57:
1261                    iName = "{}...".format(iName[:54])  # right trim for a long string
1262
1263                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1264                    self.iList[iType][instrument]["ticker"],
1265                    iName,
1266                    self.iList[iType][instrument]["figi"],
1267                    self.iList[iType][instrument]["currency"],
1268                    self.iList[iType][instrument]["lot"],
1269                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1270                ))
1271
1272        infoText = "".join(info)
1273
1274        if show:
1275            uLogger.info(infoText)
1276
1277        if self.instrumentsFile:
1278            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1279                fH.write(infoText)
1280
1281            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1282
1283        return infoText
1284
1285    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1286        """
1287        This method search and show information about instruments by part of its ticker, FIGI or name.
1288        If `searchResultsFile` string is not empty then also save information to this file.
1289
1290        :param pattern: string with part of ticker, FIGI or instrument's name.
1291        :param show: if `True` then print results to console, if `False` - return list of result only.
1292        :return: list of dictionaries with all found instruments.
1293        """
1294        if not self.iList:
1295            self.iList = self.Listing()
1296
1297        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1298        compiledPattern = re.compile(pattern, re.IGNORECASE)
1299
1300        for iType in self.iList:
1301            for instrument in self.iList[iType].values():
1302                searchResult = compiledPattern.search(" ".join(
1303                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1304                ))
1305
1306                if searchResult:
1307                    searchResults[iType][instrument["ticker"]] = instrument
1308
1309        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1310        info = [
1311            "# Search results\n\n",
1312            "* **Search pattern:** [{}]\n".format(pattern),
1313            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1314            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1315        ]
1316        infoShort = info[:]
1317
1318        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1319        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1320        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1321
1322        if resultsLen == 0:
1323            info.append("\nNo results\n")
1324            infoShort.append("\nNo results\n")
1325            uLogger.warning("No results. Try changing your search pattern.")
1326
1327        else:
1328            for iType in searchResults:
1329                iTypeValuesCount = len(searchResults[iType].values())
1330                if iTypeValuesCount > 0:
1331                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1332                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1333
1334                    for instrument in searchResults[iType].values():
1335                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1336                            instrument["type"],
1337                            instrument["ticker"],
1338                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1339                            instrument["figi"],
1340                        ))
1341
1342                    if iTypeValuesCount <= 5:
1343                        infoShort.extend(info[-iTypeValuesCount:])
1344
1345                    else:
1346                        infoShort.extend(info[-5:])
1347                        infoShort.append(skippedLine)
1348
1349        infoText = "".join(info)
1350        infoTextShort = "".join(infoShort)
1351
1352        if show:
1353            uLogger.info(infoTextShort)
1354            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1355
1356        if self.searchResultsFile:
1357            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1358                fH.write(infoText)
1359
1360            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1361
1362        return searchResults
1363
1364    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1365        """
1366        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1367
1368        :param instruments: list of strings with tickers or FIGIs.
1369        :return: list with unique instrument FIGIs only.
1370        """
1371        requestedInstruments = []
1372        for iName in instruments:
1373            if iName not in self.aliases.keys():
1374                if iName not in requestedInstruments:
1375                    requestedInstruments.append(iName)
1376
1377            else:
1378                if iName not in requestedInstruments:
1379                    if self.aliases[iName] not in requestedInstruments:
1380                        requestedInstruments.append(self.aliases[iName])
1381
1382        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1383
1384        onlyUniqueFIGIs = []
1385        for iName in requestedInstruments:
1386            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1387                continue
1388
1389            self.ticker = iName
1390            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1391
1392            if not iData:
1393                self.ticker = ""
1394                self.figi = iName
1395
1396                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1397
1398                if not iData:
1399                    self.figi = ""
1400                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1401
1402            if iData and iData["figi"] not in onlyUniqueFIGIs:
1403                onlyUniqueFIGIs.append(iData["figi"])
1404
1405        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1406
1407        return onlyUniqueFIGIs
1408
1409    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1410        """
1411        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1412        See limits: https://tinkoff.github.io/investAPI/limits/
1413        If `pricesFile` string is not empty then also save information to this file.
1414
1415        :param instruments: list of strings with tickers or FIGIs.
1416        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1417        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1418                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1419        """
1420        if instruments is None or not instruments:
1421            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1422            raise Exception("Ticker or FIGI required")
1423
1424        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1425
1426        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1427
1428        iList = []  # trying to get info and current prices about all unique instruments:
1429        for self.figi in onlyUniqueFIGIs:
1430            iData = self.SearchByFIGI(requestPrice=True)
1431            iList.append(iData)
1432
1433        self.ShowListOfPrices(iList, show)
1434
1435        return iList
1436
1437    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1438        """
1439        Show table contains current prices of given instruments.
1440
1441        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1442                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1443        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1444        :return: multilines text in Markdown format as a table contains current prices.
1445        """
1446        infoText = ""
1447
1448        if show or self.pricesFile:
1449            info = [
1450                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1451                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1452                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1453            ]
1454
1455            for item in iList:
1456                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1457                    item["ticker"],
1458                    item["figi"],
1459                    item["type"],
1460                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1461                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1462                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1463                    "{} / {}".format(
1464                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1465                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1466                    ),
1467                    "{} / {}".format(
1468                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1469                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1470                    ),
1471                    item["currency"],
1472                ))
1473
1474            infoText = "".join(info)
1475
1476            if show:
1477                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1478
1479            if self.pricesFile:
1480                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1481                    fH.write(infoText)
1482
1483                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1484
1485        return infoText
1486
1487    def RequestTradingStatus(self) -> dict:
1488        """
1489        Requesting trading status for the instrument defined by `figi` variable.
1490        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1491        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1492
1493        :return: dictionary with trading status attributes. Response example:
1494                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1495                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1496        """
1497        if self.figi is None or not self.figi:
1498            uLogger.error("Variable `figi` must be defined for using this method!")
1499            raise Exception("FIGI required")
1500
1501        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1502
1503        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1504        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1505        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1506
1507        if self.moreDebug:
1508            uLogger.debug("Records about current trading status successfully received")
1509
1510        return tradingStatus
1511
1512    def RequestPortfolio(self) -> dict:
1513        """
1514        Requesting actual user's portfolio for current `accountId`.
1515        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1516        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1517
1518        :return: dictionary with user's portfolio.
1519        """
1520        if self.accountId is None or not self.accountId:
1521            uLogger.error("Variable `accountId` must be defined for using this method!")
1522            raise Exception("Account ID required")
1523
1524        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1525
1526        self.body = str({"accountId": self.accountId})
1527        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1528        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1529
1530        if self.moreDebug:
1531            uLogger.debug("Records about user's portfolio successfully received")
1532
1533        return rawPortfolio
1534
1535    def RequestPositions(self) -> dict:
1536        """
1537        Requesting open positions by currencies and instruments for current `accountId`.
1538        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1539        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1540
1541        :return: dictionary with open positions by instruments.
1542        """
1543        if self.accountId is None or not self.accountId:
1544            uLogger.error("Variable `accountId` must be defined for using this method!")
1545            raise Exception("Account ID required")
1546
1547        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1548
1549        self.body = str({"accountId": self.accountId})
1550        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1551        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1552
1553        if self.moreDebug:
1554            uLogger.debug("Records about current open positions successfully received")
1555
1556        return rawPositions
1557
1558    def RequestPendingOrders(self) -> list:
1559        """
1560        Requesting current actual pending orders for current `accountId`.
1561        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1562        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1563
1564        :return: list of dictionaries with pending orders.
1565        """
1566        if self.accountId is None or not self.accountId:
1567            uLogger.error("Variable `accountId` must be defined for using this method!")
1568            raise Exception("Account ID required")
1569
1570        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1571
1572        self.body = str({"accountId": self.accountId})
1573        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1574        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1575
1576        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1577
1578        return rawOrders
1579
1580    def RequestStopOrders(self) -> list:
1581        """
1582        Requesting current actual stop orders for current `accountId`.
1583        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1584        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1585
1586        :return: list of dictionaries with stop orders.
1587        """
1588        if self.accountId is None or not self.accountId:
1589            uLogger.error("Variable `accountId` must be defined for using this method!")
1590            raise Exception("Account ID required")
1591
1592        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1593
1594        self.body = str({"accountId": self.accountId})
1595        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1596        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1597
1598        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1599
1600        return rawStopOrders
1601
1602    def Overview(self, show: bool = False, details: str = "full") -> dict:
1603        """
1604        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1605        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1606        are defined then also save information to file.
1607
1608        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1609        many requests about the state of the portfolio, and then, based on the received data, a large number
1610        of calculation and statistics are collected.
1611
1612        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1613        :param details: how detailed should the information be? You should specify one of strings:
1614                        `full` - shows full available information about portfolio status (by default),
1615                        `positions` - shows only open positions,
1616                        `digest` - show a short digest of the portfolio status,
1617                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1618                        `orders` - shows only sections of open limits and stop orders.
1619        :return: dictionary with client's raw portfolio and some statistics.
1620        """
1621        if self.accountId is None or not self.accountId:
1622            uLogger.error("Variable `accountId` must be defined for using this method!")
1623            raise Exception("Account ID required")
1624
1625        view = {
1626            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1627                "headers": {},  # list of dictionaries, response headers without "positions" section
1628                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1629                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1630                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1631                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1632                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1633                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1634                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1635                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1636                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1637            },
1638            "stat": {  # --- some statistics calculated using "raw" sections:
1639                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1640                "availableRUB": 0.,  # available rubles (without other currencies)
1641                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1642                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1643                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1644                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1645                "sharesCostRUB": 0.,  # costs of all shares in RUB
1646                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1647                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1648                "futuresCostRUB": 0.,  # costs of all futures in RUB
1649                "Currencies": [],  # list of dictionaries of all currencies statistics
1650                "Shares": [],  # list of dictionaries of all shares statistics
1651                "Bonds": [],  # list of dictionaries of all bonds statistics
1652                "Etfs": [],  # list of dictionaries of all etfs statistics
1653                "Futures": [],  # list of dictionaries of all futures statistics
1654                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1655                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1656                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1657                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1658                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1659            },
1660            "analytics": {  # --- some analytics of portfolio:
1661                "distrByAssets": {},  # portfolio distribution by assets
1662                "distrByCompanies": {},  # portfolio distribution by companies
1663                "distrBySectors": {},  # portfolio distribution by sectors
1664                "distrByCurrencies": {},  # portfolio distribution by currencies
1665                "distrByCountries": {},  # portfolio distribution by countries
1666            }
1667        }
1668
1669        details = details.lower()
1670        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1671        if details not in availableDetails:
1672            details = "full"
1673            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1674
1675        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1676
1677        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1678        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1679        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1680        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1681
1682        # save response headers without "positions" section:
1683        for key in portfolioResponse.keys():
1684            if key != "positions":
1685                view["raw"]["headers"][key] = portfolioResponse[key]
1686
1687            else:
1688                continue
1689
1690        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1691        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1692        for item in portfolioResponse["positions"]:
1693            if item["instrumentType"] == "currency":
1694                self.figi = item["figi"]
1695                curr = self.SearchByFIGI(requestPrice=False)
1696
1697                # current price of currency in RUB:
1698                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1699                    "name": curr["name"],
1700                    "currentPrice": NanoToFloat(
1701                        item["currentPrice"]["units"],
1702                        item["currentPrice"]["nano"]
1703                    ),
1704                }
1705
1706                view["raw"]["Currencies"].append(item)
1707
1708            elif item["instrumentType"] == "share":
1709                view["raw"]["Shares"].append(item)
1710
1711            elif item["instrumentType"] == "bond":
1712                view["raw"]["Bonds"].append(item)
1713
1714            elif item["instrumentType"] == "etf":
1715                view["raw"]["Etfs"].append(item)
1716
1717            elif item["instrumentType"] == "futures":
1718                view["raw"]["Futures"].append(item)
1719
1720            else:
1721                continue
1722
1723        # how many volume of currencies (by ISO currency name) are blocked:
1724        for item in view["raw"]["positions"]["blocked"]:
1725            blocked = NanoToFloat(item["units"], item["nano"])
1726            if blocked > 0:
1727                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1728
1729        # how many volume of instruments (by FIGI) are blocked:
1730        for item in view["raw"]["positions"]["securities"]:
1731            blocked = int(item["blocked"])
1732            if blocked > 0:
1733                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1734
1735        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1736
1737        if "rub" in allBlocked.keys():
1738            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1739
1740        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1741        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1742        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1743        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1744        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1745        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1746        view["stat"]["portfolioCostRUB"] = sum([
1747            view["stat"]["allCurrenciesCostRUB"],
1748            view["stat"]["sharesCostRUB"],
1749            view["stat"]["bondsCostRUB"],
1750            view["stat"]["etfsCostRUB"],
1751            view["stat"]["futuresCostRUB"],
1752        ])
1753
1754        # --- calculating some portfolio statistics:
1755        byComp = {}  # distribution by companies
1756        bySect = {}  # distribution by sectors
1757        byCurr = {}  # distribution by currencies (include RUB)
1758        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1759        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1760
1761        for item in portfolioResponse["positions"]:
1762            self.figi = item["figi"]
1763            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1764
1765            if instrument:
1766                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1767                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1768
1769                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1770                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1771
1772                else:
1773                    blocked = 0
1774
1775                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1776                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1777                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1778                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1779                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1780                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1781                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1782                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1783                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1784                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1785                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1786                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1787
1788                statData = {
1789                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1790                    "ticker": instrument["ticker"],  # ticker by FIGI
1791                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1792                    "volume": volume,  # available volume of instrument
1793                    "lots": lots,  # volume in lots of instrument
1794                    "direction": direction,  # direction of an instrument's position: short or long
1795                    "blocked": blocked,  # blocked volume of currency or instrument
1796                    "currentPrice": curPrice,  # current instrument's price in basic asset
1797                    "average": average,  # current average position price
1798                    "cost": cost,  # current cost of all volume of instrument in basic asset
1799                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1800                    "costRUB": costRUB,  # cost of instrument in ruble
1801                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1802                    "profit": profit,  # expected profit at current moment
1803                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1804                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1805                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1806                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1807                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1808                    "step": instrument["step"],  # minimum price increment
1809                }
1810
1811                # adding distribution by unique countries:
1812                if statData["country"] not in byCountry.keys():
1813                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1814
1815                else:
1816                    byCountry[statData["country"]]["cost"] += costRUB
1817                    byCountry[statData["country"]]["percent"] += percentCostRUB
1818
1819                if item["instrumentType"] != "currency":
1820                    # adding distribution by unique companies:
1821                    if statData["name"]:
1822                        if statData["name"] not in byComp.keys():
1823                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1824
1825                        else:
1826                            byComp[statData["name"]]["cost"] += costRUB
1827                            byComp[statData["name"]]["percent"] += percentCostRUB
1828
1829                    # adding distribution by unique sectors:
1830                    if statData["sector"] not in bySect.keys():
1831                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1832
1833                    else:
1834                        bySect[statData["sector"]]["cost"] += costRUB
1835                        bySect[statData["sector"]]["percent"] += percentCostRUB
1836
1837                # adding distribution by unique currencies:
1838                if currency not in byCurr.keys():
1839                    byCurr[currency] = {
1840                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1841                        "cost": costRUB,
1842                        "percent": percentCostRUB
1843                    }
1844
1845                else:
1846                    byCurr[currency]["cost"] += costRUB
1847                    byCurr[currency]["percent"] += percentCostRUB
1848
1849                # saving statistics for every instrument:
1850                if item["instrumentType"] == "currency":
1851                    view["stat"]["Currencies"].append(statData)
1852
1853                    # update dict with free funds for trading (total - blocked) by currencies
1854                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1855                    view["stat"]["funds"][currency] = {
1856                        "total": volume,
1857                        "totalCostRUB": costRUB,  # total volume cost in rubles
1858                        "free": volume - blocked,
1859                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1860                    }
1861
1862                elif item["instrumentType"] == "share":
1863                    view["stat"]["Shares"].append(statData)
1864
1865                elif item["instrumentType"] == "bond":
1866                    view["stat"]["Bonds"].append(statData)
1867
1868                elif item["instrumentType"] == "etf":
1869                    view["stat"]["Etfs"].append(statData)
1870
1871                elif item["instrumentType"] == "Futures":
1872                    view["stat"]["Futures"].append(statData)
1873
1874                else:
1875                    continue
1876
1877        # total changes in Russian Ruble:
1878        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1879        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1880        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1881        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1882        view["stat"]["funds"]["rub"] = {
1883            "total": view["stat"]["availableRUB"],
1884            "totalCostRUB": view["stat"]["availableRUB"],
1885            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1886            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1887        }
1888
1889        # --- pending orders sector data:
1890        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1891        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1892
1893        for item in view["raw"]["orders"]:
1894            self.figi = item["figi"]
1895
1896            if item["figi"] not in uniquePendingOrdersFIGIs:
1897                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1898
1899                uniquePendingOrdersFIGIs.append(item["figi"])
1900                uniquePendingOrders[item["figi"]] = instrument
1901
1902            else:
1903                instrument = uniquePendingOrders[item["figi"]]
1904
1905            if instrument:
1906                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1907                orderType = TKS_ORDER_TYPES[item["orderType"]]
1908                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1909                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1910
1911                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1912                if item["direction"] == "ORDER_DIRECTION_BUY":
1913                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1914
1915                else:
1916                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1917
1918                # requested price for order execution:
1919                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1920
1921                # necessary changes in percent to reach target from current price:
1922                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1923
1924                view["stat"]["orders"].append({
1925                    "orderID": item["orderId"],  # orderId number parameter of current order
1926                    "figi": item["figi"],  # FIGI identification
1927                    "ticker": instrument["ticker"],  # ticker name by FIGI
1928                    "lotsRequested": item["lotsRequested"],  # requested lots value
1929                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1930                    "currentPrice": lastPrice,  # current instrument's price for defined action
1931                    "targetPrice": target,  # requested price for order execution in base currency
1932                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1933                    "percentChanges": changes,  # changes in percent to target from current price
1934                    "currency": item["currency"],  # instrument's currency name
1935                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1936                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1937                    "status": orderState,  # order status from TKS_ORDER_STATES
1938                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1939                })
1940
1941        # --- stop orders sector data:
1942        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1943        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1944
1945        for item in view["raw"]["stopOrders"]:
1946            self.figi = item["figi"]
1947
1948            if item["figi"] not in uniqueStopOrdersFIGIs:
1949                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1950
1951                uniqueStopOrdersFIGIs.append(item["figi"])
1952                uniqueStopOrders[item["figi"]] = instrument
1953
1954            else:
1955                instrument = uniqueStopOrders[item["figi"]]
1956
1957            if instrument:
1958                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1959                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1960                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1961
1962                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1963                if "expirationTime" in item.keys():
1964                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1965                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1966
1967                else:
1968                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1969                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1970
1971                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1972                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1973                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1974
1975                else:
1976                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1977
1978                # requested price when stop-order executed:
1979                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1980
1981                # price for limit-order, set up when stop-order executed:
1982                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1983
1984                # necessary changes in percent to reach target from current price:
1985                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1986
1987                view["stat"]["stopOrders"].append({
1988                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1989                    "figi": item["figi"],  # FIGI identification
1990                    "ticker": instrument["ticker"],  # ticker name by FIGI
1991                    "lotsRequested": item["lotsRequested"],  # requested lots value
1992                    "currentPrice": lastPrice,  # current instrument's price for defined action
1993                    "targetPrice": target,  # requested price for stop-order execution in base currency
1994                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1995                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1996                    "percentChanges": changes,  # changes in percent to target from current price
1997                    "currency": item["currency"],  # instrument's currency name
1998                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1999                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2000                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2001                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2002                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2003                })
2004
2005        # --- calculating data for analytics section:
2006        # portfolio distribution by assets:
2007        view["analytics"]["distrByAssets"] = {
2008            "Ruble": {
2009                "uniques": 1,
2010                "cost": view["stat"]["availableRUB"],
2011                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2012            },
2013            "Currencies": {
2014                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2015                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2016                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2017            },
2018            "Shares": {
2019                "uniques": len(view["stat"]["Shares"]),
2020                "cost": view["stat"]["sharesCostRUB"],
2021                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2022            },
2023            "Bonds": {
2024                "uniques": len(view["stat"]["Bonds"]),
2025                "cost": view["stat"]["bondsCostRUB"],
2026                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2027            },
2028            "Etfs": {
2029                "uniques": len(view["stat"]["Etfs"]),
2030                "cost": view["stat"]["etfsCostRUB"],
2031                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2032            },
2033            "Futures": {
2034                "uniques": len(view["stat"]["Futures"]),
2035                "cost": view["stat"]["futuresCostRUB"],
2036                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2037            },
2038        }
2039
2040        # portfolio distribution by companies:
2041        view["analytics"]["distrByCompanies"]["All money cash"] = {
2042            "ticker": "",
2043            "cost": view["stat"]["allCurrenciesCostRUB"],
2044            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2045        }
2046        view["analytics"]["distrByCompanies"].update(byComp)
2047
2048        # portfolio distribution by sectors:
2049        view["analytics"]["distrBySectors"]["All money cash"] = {
2050            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2051            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2052        }
2053        view["analytics"]["distrBySectors"].update(bySect)
2054
2055        # portfolio distribution by currencies:
2056        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2057            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2058
2059            if self.moreDebug:
2060                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2061
2062        view["analytics"]["distrByCurrencies"].update(byCurr)
2063        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2064        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2065
2066        # portfolio distribution by countries:
2067        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2068            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2069
2070            if self.moreDebug:
2071                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2072
2073        view["analytics"]["distrByCountries"].update(byCountry)
2074        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2075        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2076
2077        # --- Prepare text statistics overview in human-readable:
2078        if show:
2079            # Whatever the value `details`, header not changes:
2080            info = [
2081                "# Client's portfolio\n\n",
2082                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2083                "* **Account ID:** [{}]\n".format(self.accountId),
2084            ]
2085
2086            if details in ["full", "positions", "digest"]:
2087                info.extend([
2088                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2089                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2090                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2091                        view["stat"]["totalChangesRUB"],
2092                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2093                        view["stat"]["totalChangesPercentRUB"],
2094                    ),
2095                ])
2096
2097            if details in ["full", "positions"]:
2098                info.extend([
2099                    "## Open positions\n\n",
2100                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2101                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2102                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2103                        "{:.2f} ({:.2f}) rub".format(
2104                            view["stat"]["availableRUB"],
2105                            view["stat"]["blockedRUB"],
2106                        )
2107                    )
2108                ])
2109
2110                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2111                    return [
2112                        "|                             |                                 |          |              |              |                     |                              |\n",
2113                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2114                            noTradeStr if noTradeStr else typeStr,
2115                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2116                        ),
2117                    ]
2118
2119                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2120                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2121                        "{} [{}]".format(data["ticker"], data["figi"]),
2122                        "{:.2f} ({:.2f}) {}".format(
2123                            data["volume"],
2124                            data["blocked"],
2125                            data["currency"],
2126                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2127                            data["volume"],
2128                            data["blocked"],
2129                        ),
2130                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2131                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2132                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2133                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2134                        "{}{:.2f} {} ({}{:.2f}%)".format(
2135                            "+" if data["profit"] > 0 else "",
2136                            data["profit"], data["baseCurrencyName"],
2137                            "+" if data["percentProfit"] > 0 else "",
2138                            data["percentProfit"],
2139                        ),
2140                    )
2141
2142                # --- Show currencies section:
2143                if view["stat"]["Currencies"]:
2144                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2145                    for item in view["stat"]["Currencies"]:
2146                        info.append(_InfoStr(item, showCurrencyName=True))
2147
2148                else:
2149                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2150
2151                # --- Show shares section:
2152                if view["stat"]["Shares"]:
2153                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2154
2155                    for item in view["stat"]["Shares"]:
2156                        info.append(_InfoStr(item))
2157
2158                else:
2159                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2160
2161                # --- Show bonds section:
2162                if view["stat"]["Bonds"]:
2163                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2164
2165                    for item in view["stat"]["Bonds"]:
2166                        info.append(_InfoStr(item))
2167
2168                else:
2169                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2170
2171                # --- Show etfs section:
2172                if view["stat"]["Etfs"]:
2173                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2174
2175                    for item in view["stat"]["Etfs"]:
2176                        info.append(_InfoStr(item))
2177
2178                else:
2179                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2180
2181                # --- Show futures section:
2182                if view["stat"]["Futures"]:
2183                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2184
2185                    for item in view["stat"]["Futures"]:
2186                        info.append(_InfoStr(item))
2187
2188                else:
2189                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2190
2191            if details in ["full", "orders"]:
2192                # --- Show pending orders section:
2193                if view["stat"]["orders"]:
2194                    info.extend([
2195                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2196                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2197                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2198                    ])
2199
2200                    for item in view["stat"]["orders"]:
2201                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2202                            "{} [{}]".format(item["ticker"], item["figi"]),
2203                            item["orderID"],
2204                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2205                            "{} {} ({}{:.2f}%)".format(
2206                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2207                                item["baseCurrencyName"],
2208                                "+" if item["percentChanges"] > 0 else "",
2209                                float(item["percentChanges"]),
2210                            ),
2211                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2212                            item["action"],
2213                            item["type"],
2214                            item["date"],
2215                        ))
2216
2217                else:
2218                    info.append("\n## Total pending limit-orders: 0\n")
2219
2220                # --- Show stop orders section:
2221                if view["stat"]["stopOrders"]:
2222                    info.extend([
2223                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2224                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2225                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2226                    ])
2227
2228                    for item in view["stat"]["stopOrders"]:
2229                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2230                            "{} [{}]".format(item["ticker"], item["figi"]),
2231                            item["orderID"],
2232                            item["lotsRequested"],
2233                            "{} {} ({}{:.2f}%)".format(
2234                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2235                                item["baseCurrencyName"],
2236                                "+" if item["percentChanges"] > 0 else "",
2237                                float(item["percentChanges"]),
2238                            ),
2239                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2240                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2241                            item["action"],
2242                            item["type"],
2243                            item["expType"],
2244                            item["createDate"],
2245                            item["expDate"],
2246                        ))
2247
2248                else:
2249                    info.append("\n## Total stop-orders: 0\n")
2250
2251            if details in ["full", "analytics"]:
2252                # -- Show analytics section:
2253                if view["stat"]["portfolioCostRUB"] > 0:
2254                    info.extend([
2255                        "\n# Analytics\n"
2256                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2257                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2258                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2259                            view["stat"]["totalChangesRUB"],
2260                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2261                            view["stat"]["totalChangesPercentRUB"],
2262                        ),
2263                        "\n## Portfolio distribution by assets\n"
2264                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2265                        "|------------|---------|---------|--------------------|\n",
2266                    ])
2267
2268                    for key in view["analytics"]["distrByAssets"].keys():
2269                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2270                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2271                                key,
2272                                view["analytics"]["distrByAssets"][key]["uniques"],
2273                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2274                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2275                            ))
2276
2277                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2278                    info.extend([
2279                        "\n## Portfolio distribution by companies\n"
2280                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2281                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2282                    ])
2283
2284                    for company in view["analytics"]["distrByCompanies"].keys():
2285                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2286                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2287                            info.append("| {} | {:<7} | {:<18} |\n".format(
2288                                "{}{}{}".format(
2289                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2290                                    company,
2291                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2292                                ),
2293                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2294                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2295                            ))
2296
2297                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2298                    info.extend([
2299                        "\n## Portfolio distribution by sectors\n"
2300                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2301                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2302                    ])
2303
2304                    for sector in view["analytics"]["distrBySectors"].keys():
2305                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2306                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2307                                sector,
2308                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2309                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2310                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2311                            ))
2312
2313                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2314                    info.extend([
2315                        "\n## Portfolio distribution by currencies\n"
2316                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2317                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2318                    ])
2319
2320                    for curr in view["analytics"]["distrByCurrencies"].keys():
2321                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2322                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2323                            info.append("| {} | {:<7} | {:<18} |\n".format(
2324                                "[{}] {}{}".format(
2325                                    curr,
2326                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2327                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2328                                ),
2329                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2330                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2331                            ))
2332
2333                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2334                    info.extend([
2335                        "\n## Portfolio distribution by countries\n"
2336                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2337                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2338                    ])
2339
2340                    for country in view["analytics"]["distrByCountries"].keys():
2341                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2342                            nameLen = len(country)
2343                            info.append("| {} | {:<7} | {:<18} |\n".format(
2344                                "{}{}".format(
2345                                    country,
2346                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2347                                ),
2348                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2349                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2350                            ))
2351
2352            infoText = "".join(info)
2353
2354            uLogger.info(infoText)
2355
2356            if details == "full" and self.overviewFile:
2357                filename = self.overviewFile
2358
2359            elif details == "digest" and self.overviewDigestFile:
2360                filename = self.overviewDigestFile
2361
2362            elif details == "positions" and self.overviewPositionsFile:
2363                filename = self.overviewPositionsFile
2364
2365            elif details == "orders" and self.overviewOrdersFile:
2366                filename = self.overviewOrdersFile
2367
2368            elif details == "analytics" and self.overviewAnalyticsFile:
2369                filename = self.overviewAnalyticsFile
2370
2371            else:
2372                filename = ""
2373
2374            if filename:
2375                with open(filename, "w", encoding="UTF-8") as fH:
2376                    fH.write(infoText)
2377
2378                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2379
2380        return view
2381
2382    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2383        """
2384        Returns history operations between two given dates for current `accountId`.
2385        If `reportFile` string is not empty then also save human-readable report.
2386        Shows some statistical data of closed positions.
2387
2388        :param start: see docstring in `GetDatesAsString()` method
2389        :param end: see docstring in `GetDatesAsString()` method
2390        :param show: if `True` then also prints all records to the console.
2391        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2392        :return: original list of dictionaries with history of deals records from API ("operations" key):
2393                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2394                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2395        """
2396        if self.accountId is None or not self.accountId:
2397            uLogger.error("Variable `accountId` must be defined for using this method!")
2398            raise Exception("Account ID required")
2399
2400        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2401
2402        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2403
2404        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2405        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2406        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2407        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2408        customStat = {}  # custom statistics in additional to responseJSON
2409
2410        # --- output report in human-readable format:
2411        if show or self.reportFile:
2412            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2413            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2414            nextDay = ""
2415
2416            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2417
2418            if len(ops) > 0:
2419                customStat = {
2420                    "opsCount": 0,  # total operations count
2421                    "buyCount": 0,  # buy operations
2422                    "sellCount": 0,  # sell operations
2423                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2424                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2425                    "payIn": {"rub": 0.},  # Deposit brokerage account
2426                    "payOut": {"rub": 0.},  # Withdrawals
2427                    "divs": {"rub": 0.},  # Dividends income
2428                    "coupons": {"rub": 0.},  # Coupon's income
2429                    "brokerCom": {"rub": 0.},  # Service commissions
2430                    "serviceCom": {"rub": 0.},  # Service commissions
2431                    "marginCom": {"rub": 0.},  # Margin commissions
2432                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2433                }
2434
2435                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2436                for item in ops:
2437                    if item["state"] == "OPERATION_STATE_EXECUTED":
2438                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2439
2440                        # count buy operations:
2441                        if "_BUY" in item["operationType"]:
2442                            customStat["buyCount"] += 1
2443
2444                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2445                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2446
2447                            else:
2448                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2449
2450                        # count sell operations:
2451                        elif "_SELL" in item["operationType"]:
2452                            customStat["sellCount"] += 1
2453
2454                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2455                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2456
2457                            else:
2458                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2459
2460                        # count incoming operations:
2461                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2462                            if item["payment"]["currency"] in customStat["payIn"].keys():
2463                                customStat["payIn"][item["payment"]["currency"]] += payment
2464
2465                            else:
2466                                customStat["payIn"][item["payment"]["currency"]] = payment
2467
2468                        # count withdrawals operations:
2469                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2470                            if item["payment"]["currency"] in customStat["payOut"].keys():
2471                                customStat["payOut"][item["payment"]["currency"]] += payment
2472
2473                            else:
2474                                customStat["payOut"][item["payment"]["currency"]] = payment
2475
2476                        # count dividends income:
2477                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2478                            if item["payment"]["currency"] in customStat["divs"].keys():
2479                                customStat["divs"][item["payment"]["currency"]] += payment
2480
2481                            else:
2482                                customStat["divs"][item["payment"]["currency"]] = payment
2483
2484                        # count coupon's income:
2485                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2486                            if item["payment"]["currency"] in customStat["coupons"].keys():
2487                                customStat["coupons"][item["payment"]["currency"]] += payment
2488
2489                            else:
2490                                customStat["coupons"][item["payment"]["currency"]] = payment
2491
2492                        # count broker commissions:
2493                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2494                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2495                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2496
2497                            else:
2498                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2499
2500                        # count service commissions:
2501                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2502                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2503                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2504
2505                            else:
2506                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2507
2508                        # count margin commissions:
2509                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2510                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2511                                customStat["marginCom"][item["payment"]["currency"]] += payment
2512
2513                            else:
2514                                customStat["marginCom"][item["payment"]["currency"]] = payment
2515
2516                        # count withholding taxes:
2517                        elif "_TAX" in item["operationType"]:
2518                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2519                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2520
2521                            else:
2522                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2523
2524                        else:
2525                            continue
2526
2527                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2528
2529                # --- view "Actions" lines:
2530                info.extend([
2531                    "| Report sections            |                               |                              |                      |                        |\n",
2532                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2533                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2534                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2535                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2536                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2537                    ),
2538                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2539                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2540                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2541                    ),
2542                ])
2543
2544                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2545                for key in opsKeys:
2546                    if key == "rub":
2547                        continue
2548
2549                    info.extend([
2550                        "|                            |                               | {:<28} |                      |                        |\n".format(
2551                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2552                        ),
2553                        "|                            |                               | {:<28} |                      |                        |\n".format(
2554                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2555                        ),
2556                    ])
2557
2558                info.append(splitLine1)
2559
2560                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2561                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2562                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2563                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2564                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2565                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2566                    )
2567
2568                # --- view "Payments" lines:
2569                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2570                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2571
2572                for key in paymentsKeys:
2573                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2574
2575                info.append(splitLine1)
2576
2577                # --- view "Commissions and taxes" lines:
2578                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2579                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2580
2581                for key in comKeys:
2582                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2583
2584                info.append(splitLine1)
2585
2586                info.extend([
2587                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2588                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2589                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2590                ])
2591
2592            else:
2593                info.append("Broker returned no operations during this period\n")
2594
2595            # --- view "Operations" section:
2596            for item in ops:
2597                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2598                    continue
2599
2600                else:
2601                    self.figi = item["figi"] if item["figi"] else ""
2602                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2603                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2604
2605                    # group of deals during one day:
2606                    if nextDay and item["date"].split("T")[0] != nextDay:
2607                        info.append(splitLine2)
2608                        nextDay = ""
2609
2610                    else:
2611                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2612
2613                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2614                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2615                        self.figi if self.figi else "—",
2616                        instrument["ticker"] if instrument else "—",
2617                        instrument["type"] if instrument else "—",
2618                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2619                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2620                        TKS_OPERATION_STATES[item["state"]],
2621                        TKS_OPERATION_TYPES[item["operationType"]],
2622                    ))
2623
2624            infoText = "".join(info)
2625
2626            if show:
2627                if self.moreDebug:
2628                    uLogger.debug("Records about history of a client's operations successfully received")
2629
2630                uLogger.info(infoText)
2631
2632            if self.reportFile:
2633                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2634                    fH.write(infoText)
2635
2636                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2637
2638        return ops, customStat
2639
2640    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2641        """
2642        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2643
2644        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2645        Warning! Broker server used ISO UTC time by default.
2646
2647        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2648        Also, `historyFile` used to update history with `onlyMissing` parameter.
2649
2650        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2651
2652        :param start: see docstring in `GetDatesAsString()` method.
2653        :param end: see docstring in `GetDatesAsString()` method.
2654        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2655                         `"hour"`, `"day"`. Default: `"hour"`.
2656        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2657                            False by default. Warning! History appends only from last candle to current time
2658                            with always update last candle!
2659        :param csvSep: separator if csv-file is used, `,` by default.
2660        :param show: if `True` then also prints Pandas DataFrame to the console.
2661        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2662                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2663        """
2664        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2665        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2666        history = None  # empty pandas object for history
2667
2668        if interval not in TKS_CANDLE_INTERVALS.keys():
2669            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2670            raise Exception("Incorrect value")
2671
2672        if not (self.ticker or self.figi):
2673            uLogger.error("Ticker or FIGI must be defined!")
2674            raise Exception("Ticker or FIGI required")
2675
2676        if self.ticker and not self.figi:
2677            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2678            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2679
2680        if self.figi and not self.ticker:
2681            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2682            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2683
2684        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2685        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2686        if interval.lower() != "day":
2687            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2688
2689        delta = dtEnd - dtStart  # current UTC time minus last time in file
2690        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2691
2692        # calculate history length in candles:
2693        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2694        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2695            length += 1  # to avoid fraction time
2696
2697        # calculate data blocks count:
2698        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2699
2700        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2701        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2702        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2703        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2704        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2705
2706        tempOld = None  # pandas object for old history, if --only-missing key present
2707        lastTime = None  # datetime object of last old candle in file
2708
2709        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2710            uLogger.debug("--only-missing key present, add only last missing candles...")
2711            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2712
2713            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2714
2715            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2716            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2717            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2718            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2719
2720            # get last datetime object from last string in file or minus 1 delta if file is empty:
2721            if len(tempOld) > 0:
2722                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2723
2724            else:
2725                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2726
2727            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2728
2729        responseJSONs = []  # raw history blocks of data
2730
2731        blockEnd = dtEnd
2732        for item in range(blocks):
2733            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2734            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2735
2736            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2737                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2738            ))
2739
2740            if blockStart == blockEnd:
2741                uLogger.debug("Skipped this zero-length block...")
2742
2743            else:
2744                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2745                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2746                self.body = str({
2747                    "figi": self.figi,
2748                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2749                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2750                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2751                })
2752                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2753
2754                if "code" in responseJSON.keys():
2755                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2756
2757                else:
2758                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2759                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2760
2761                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2762
2763            blockEnd = blockStart
2764
2765        printCount = len(responseJSONs)  # candles to show in console
2766        if responseJSONs:
2767            tempHistory = pd.DataFrame(
2768                data={
2769                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2770                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2771                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2772                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2773                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2774                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2775                    "volume": [int(item["volume"]) for item in responseJSONs],
2776                },
2777                index=range(len(responseJSONs)),
2778                columns=["date", "time", "open", "high", "low", "close", "volume"],
2779            )
2780            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2781            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2782
2783            # append only newest candles to old history if --only-missing key present:
2784            if onlyMissing and tempOld is not None and lastTime is not None:
2785                index = 0  # find start index in tempHistory data:
2786
2787                for i, item in tempHistory.iterrows():
2788                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2789
2790                    if curTime == lastTime:
2791                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2792                        index = i
2793                        printCount = index + 1
2794                        break
2795
2796                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2797
2798            else:
2799                history = tempHistory  # if no `--only-missing` key then load full data from server
2800
2801            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2802
2803        if history is not None and not history.empty:
2804            if show:
2805                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2806                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2807                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2808                ))
2809
2810        else:
2811            uLogger.warning("Received an empty candles history!")
2812
2813        if self.historyFile is not None:
2814            if history is not None and not history.empty:
2815                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2816                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2817
2818            else:
2819                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2820
2821        else:
2822            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2823
2824        return history
2825
2826    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2827        """
2828        Load candles history from csv-file and return Pandas DataFrame object.
2829
2830        See also: `History()` and `ShowHistoryChart()` methods.
2831
2832        :param filePath: path to csv-file to open.
2833        """
2834        loadedHistory = None  # init candles data object
2835
2836        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2837
2838        if os.path.exists(filePath):
2839            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2840
2841            tfStr = self.priceModel.FormattedDelta(
2842                self.priceModel.timeframe,
2843                "{days} days {hours}h {minutes}m {seconds}s",
2844            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2845                self.priceModel.timeframe,
2846                "{hours}h {minutes}m {seconds}s",
2847            )
2848
2849            if loadedHistory is not None and not loadedHistory.empty:
2850                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2851                    len(loadedHistory),
2852                    tfStr,
2853                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2854                )
2855
2856            else:
2857                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2858
2859        else:
2860            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2861
2862        return loadedHistory
2863
2864    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2865        """
2866        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2867
2868        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2869        Default: `index.html` (both for interact and non-interact candlesticks chart).
2870
2871        See also: `History()` and `LoadHistory()` methods.
2872
2873        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2874        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2875                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2876                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2877                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2878        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2879                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2880        """
2881        if isinstance(candles, str):
2882            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2883            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2884
2885        elif isinstance(candles, pd.DataFrame):
2886            self.priceModel.prices = candles  # set candles chain from variable
2887            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2888
2889            if "datetime" not in candles.columns:
2890                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2891
2892        else:
2893            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2894            raise Exception("Incorrect value")
2895
2896        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2897
2898        if interact:
2899            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2900
2901            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2902
2903        else:
2904            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2905
2906            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2907
2908        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2909
2910    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2911        """
2912        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2913        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2914
2915        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2916
2917        :param operation: string "Buy" or "Sell".
2918        :param lots: volume, integer count of lots >= 1.
2919        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2920        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2921        :param expDate: string "Undefined" by default or local date in future,
2922                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2923        :return: JSON with response from broker server.
2924        """
2925        if self.accountId is None or not self.accountId:
2926            uLogger.error("Variable `accountId` must be defined for using this method!")
2927            raise Exception("Account ID required")
2928
2929        if operation is None or not operation or operation not in ("Buy", "Sell"):
2930            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2931            raise Exception("Incorrect value")
2932
2933        if lots is None or lots < 1:
2934            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2935            lots = 1
2936
2937        if tp is None or tp < 0:
2938            tp = 0
2939
2940        if sl is None or sl < 0:
2941            sl = 0
2942
2943        if expDate is None or not expDate:
2944            expDate = "Undefined"
2945
2946        if not (self.ticker or self.figi):
2947            uLogger.error("Ticker or FIGI must be defined!")
2948            raise Exception("Ticker or FIGI required")
2949
2950        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2951        self.ticker = instrument["ticker"]
2952        self.figi = instrument["figi"]
2953
2954        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2955
2956        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2957        self.body = str({
2958            "figi": self.figi,
2959            "quantity": str(lots),
2960            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2961            "accountId": str(self.accountId),
2962            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2963        })
2964        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2965
2966        if "orderId" in response.keys():
2967            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2968                operation, response["orderId"],
2969                self.ticker, self.figi, lots,
2970                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2971                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2972                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2973            ))
2974
2975            if tp > 0:
2976                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2977
2978            if sl > 0:
2979                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2980
2981        else:
2982            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2983
2984        return response
2985
2986    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2987        """
2988        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2989        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2990
2991        See also: `Order()` and `Trade()` docstrings.
2992
2993        :param lots: volume, integer count of lots >= 1.
2994        :param tp: float > 0, take profit price of stop-order.
2995        :param sl: float > 0, stop loss price of stop-order.
2996        :param expDate: it's a local date in future.
2997                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2998        :return: JSON with response from broker server.
2999        """
3000        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3001
3002    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3003        """
3004        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3005        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3006
3007        See also: `Order()` and `Trade()` docstrings.
3008
3009        :param lots: volume, integer count of lots >= 1.
3010        :param tp: float > 0, take profit price of stop-order.
3011        :param sl: float > 0, stop loss price of stop-order.
3012        :param expDate: it's a local date in the future.
3013                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3014        :return: JSON with response from broker server.
3015        """
3016        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3017
3018    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3019        """
3020        Close position of given instruments.
3021
3022        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3023        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3024                         This avoids unnecessary downloading data from the server.
3025        """
3026        if instruments is None or not instruments:
3027            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3028            raise Exception("Ticker or FIGI required")
3029
3030        if isinstance(instruments, str):
3031            instruments = [instruments]
3032
3033        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3034        if uniqueInstruments:
3035            if portfolio is None or not portfolio:
3036                portfolio = self.Overview(show=False)
3037
3038            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3039            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3040
3041            for self.figi in uniqueInstruments:
3042                if self.figi not in allOpened:
3043                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3044                    continue
3045
3046                # search open trade info about instrument by ticker:
3047                instrument = {}
3048                for iType in TKS_INSTRUMENTS:
3049                    if instrument:
3050                        break
3051
3052                    for item in portfolio["stat"][iType]:
3053                        if item["figi"] == self.figi:
3054                            instrument = item
3055                            break
3056
3057                if instrument:
3058                    self.ticker = instrument["ticker"]
3059                    self.figi = instrument["figi"]
3060
3061                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3062                        self.ticker,
3063                        self.figi,
3064                        int(instrument["volume"]),
3065                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3066                    ))
3067
3068                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3069
3070                    if tradeLots > 0:
3071                        if instrument["blocked"] > 0:
3072                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3073                                instrument["blocked"],
3074                                self.ticker,
3075                                tradeLots,
3076                            ))
3077
3078                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3079                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3080
3081                    else:
3082                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3083
3084    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3085        """
3086        Close all positions of given instruments with defined type.
3087
3088        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3089        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3090                         This avoids unnecessary downloading data from the server.
3091        """
3092        if iType not in TKS_INSTRUMENTS:
3093            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3094
3095        else:
3096            if portfolio is None or not portfolio:
3097                portfolio = self.Overview(show=False)
3098
3099            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3100            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3101
3102            if tickers and portfolio:
3103                self.CloseTrades(tickers, portfolio)
3104
3105            else:
3106                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3107
3108    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3109        """
3110        Universal method to create market or limit orders with all available parameters for current `accountId`.
3111        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3112
3113        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3114        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3115
3116        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3117        then broker immediately open market order as you can do simple --buy or --sell operations!
3118
3119        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3120        When current price will go up or down to target price value then broker opens a limit order.
3121        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3122
3123        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3124
3125        :param operation: string "Buy" or "Sell".
3126        :param orderType: string "Limit" or "Stop".
3127        :param lots: volume, integer count of lots >= 1.
3128        :param targetPrice: target price > 0. This is open trade price for limit order.
3129        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3130                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3131        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3132                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3133                         Stop loss order always executed by market price.
3134        :param expDate: string "Undefined" by default or local date in future.
3135                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3136                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3137                        A limit order has no expiration date, it lasts until the end of the trading day.
3138        :return: JSON with response from broker server.
3139        """
3140        if self.accountId is None or not self.accountId:
3141            uLogger.error("Variable `accountId` must be defined for using this method!")
3142            raise Exception("Account ID required")
3143
3144        if operation is None or not operation or operation not in ("Buy", "Sell"):
3145            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3146            raise Exception("Incorrect value")
3147
3148        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3149            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3150            raise Exception("Incorrect value")
3151
3152        if lots is None or lots < 1:
3153            uLogger.error("You must define trade volume > 0: integer count of lots!")
3154            raise Exception("Incorrect value")
3155
3156        if targetPrice is None or targetPrice <= 0:
3157            uLogger.error("Target price for limit-order must be greater than 0!")
3158            raise Exception("Incorrect value")
3159
3160        if limitPrice is None or limitPrice <= 0:
3161            limitPrice = targetPrice
3162
3163        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3164            stopType = "Limit"
3165
3166        if expDate is None or not expDate:
3167            expDate = "Undefined"
3168
3169        if not (self.ticker or self.figi):
3170            uLogger.error("Tocker or FIGI must be defined!")
3171            raise Exception("Ticker or FIGI required")
3172
3173        response = {}
3174        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3175        self.ticker = instrument["ticker"]
3176        self.figi = instrument["figi"]
3177
3178        if orderType == "Limit":
3179            uLogger.debug(
3180                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3181                    self.ticker, self.figi,
3182                    operation, lots, targetPrice, instrument["currency"],
3183                ))
3184
3185            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3186            self.body = str({
3187                "figi": self.figi,
3188                "quantity": str(lots),
3189                "price": FloatToNano(targetPrice),
3190                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3191                "accountId": str(self.accountId),
3192                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3193            })
3194            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3195
3196            if "orderId" in response.keys():
3197                uLogger.info(
3198                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3199                        response["orderId"],
3200                        self.ticker, self.figi,
3201                        operation, lots, targetPrice, instrument["currency"],
3202                    ))
3203
3204                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3205                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3206                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3207                            targetPrice, instrument["currency"],
3208                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3209                        ))
3210
3211                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3212                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3213                            targetPrice, instrument["currency"],
3214                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3215                        ))
3216
3217            else:
3218                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3219
3220        if orderType == "Stop":
3221            uLogger.debug(
3222                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3223                    self.ticker, self.figi,
3224                    operation, lots,
3225                    targetPrice, instrument["currency"],
3226                    limitPrice, instrument["currency"],
3227                    stopType, expDate,
3228                ))
3229
3230            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3231            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3232            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3233
3234            body = {
3235                "figi": self.figi,
3236                "quantity": str(lots),
3237                "price": FloatToNano(limitPrice),
3238                "stopPrice": FloatToNano(targetPrice),
3239                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3240                "accountId": str(self.accountId),
3241                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3242                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3243            }
3244
3245            if expDateUTC:
3246                body["expireDate"] = expDateUTC
3247
3248            self.body = str(body)
3249            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3250
3251            if "stopOrderId" in response.keys():
3252                uLogger.info(
3253                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3254                        response["stopOrderId"],
3255                        self.ticker, self.figi,
3256                        operation, lots,
3257                        targetPrice, instrument["currency"],
3258                        limitPrice, instrument["currency"],
3259                        TKS_STOP_ORDER_TYPES[stopOrderType],
3260                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3261                    ))
3262
3263                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3264                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3265                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3266                            targetPrice, instrument["currency"],
3267                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3268                        ))
3269
3270                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3271                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3272                            targetPrice, instrument["currency"],
3273                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3274                        ))
3275
3276            else:
3277                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3278
3279        return response
3280
3281    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3282        """
3283        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3284        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3285        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3286        See also: `Order()` docstring.
3287
3288        :param lots: volume, integer count of lots >= 1.
3289        :param targetPrice: target price > 0. This is open trade price for limit order.
3290        :return: JSON with response from broker server.
3291        """
3292        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3293
3294    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3295        """
3296        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3297        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3298        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3299        target price value then broker opens a limit order. See also: `Order()` docstring.
3300
3301        :param lots: volume, integer count of lots >= 1.
3302        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3303        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3304                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3305        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3306                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3307        :param expDate: string "Undefined" by default or local date in future.
3308                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3309                        This date is converting to UTC format for server.
3310        :return: JSON with response from broker server.
3311        """
3312        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3313
3314    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3315        """
3316        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3317        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3318        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3319        See also: `Order()` docstring.
3320
3321        :param lots: volume, integer count of lots >= 1.
3322        :param targetPrice: target price > 0. This is open trade price for limit order.
3323        :return: JSON with response from broker server.
3324        """
3325        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3326
3327    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3328        """
3329        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3330        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3331        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3332        target price value then broker opens a limit order. See also: `Order()` docstring.
3333
3334        :param lots: volume, integer count of lots >= 1.
3335        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3336        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3337                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3338        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3339                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3340        :param expDate: string "Undefined" by default or local date in future.
3341                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3342                        This date is converting to UTC format for server.
3343        :return: JSON with response from broker server.
3344        """
3345        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3346
3347    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3348        """
3349        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3350
3351        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3352        :param allOrdersIDs: pre-received lists of all active pending orders.
3353                             This avoids unnecessary downloading data from the server.
3354        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3355        """
3356        if self.accountId is None or not self.accountId:
3357            uLogger.error("Variable `accountId` must be defined for using this method!")
3358            raise Exception("Account ID required")
3359
3360        if orderIDs:
3361            if allOrdersIDs is None or not allOrdersIDs:
3362                rawOrders = self.RequestPendingOrders()
3363                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3364
3365            if allStopOrdersIDs is None or not allStopOrdersIDs:
3366                rawStopOrders = self.RequestStopOrders()
3367                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3368
3369            for orderID in orderIDs:
3370                idInPendingOrders = orderID in allOrdersIDs
3371                idInStopOrders = orderID in allStopOrdersIDs
3372
3373                if not (idInPendingOrders or idInStopOrders):
3374                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3375                    continue
3376
3377                else:
3378                    if idInPendingOrders:
3379                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3380
3381                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3382                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3383                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3384                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3385
3386                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3387                            if self.moreDebug:
3388                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3389
3390                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3391
3392                        else:
3393                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3394
3395                    elif idInStopOrders:
3396                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3397
3398                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3399                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3400                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3401                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3402
3403                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3404                            if self.moreDebug:
3405                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3406
3407                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3408
3409                        else:
3410                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3411
3412                    else:
3413                        continue
3414
3415    def CloseAllOrders(self) -> None:
3416        """
3417        Gets a list of open pending and stop orders and cancel it all.
3418        """
3419        rawOrders = self.RequestPendingOrders()
3420        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3421        lenOrders = len(allOrdersIDs)
3422
3423        rawStopOrders = self.RequestStopOrders()
3424        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3425        lenSOrders = len(allStopOrdersIDs)
3426
3427        if lenOrders > 0 or lenSOrders > 0:
3428            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3429
3430            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3431
3432        else:
3433            uLogger.info("Orders not found, nothing to cancel.")
3434
3435    def CloseAll(self, *args) -> None:
3436        """
3437        Close all available (not blocked) opened trades and orders.
3438
3439        Also, you can select one or more keywords case-insensitive:
3440        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3441
3442        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3443        """
3444        overview = self.Overview(show=False)  # get all open trades info
3445
3446        if len(args) == 0:
3447            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3448            self.CloseAllOrders()  # close all pending and stop orders
3449
3450            for iType in TKS_INSTRUMENTS:
3451                if iType != "Currencies":
3452                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3453
3454        else:
3455            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3456            lowerArgs = [x.lower() for x in args]
3457
3458            if "orders" in lowerArgs:
3459                self.CloseAllOrders()  # close all pending and stop orders
3460
3461            for iType in TKS_INSTRUMENTS:
3462                if iType.lower() in lowerArgs and iType != "Currencies":
3463                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3464
3465    @staticmethod
3466    def ParseOrderParameters(operation, **inputParameters):
3467        """
3468        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3469
3470        :param operation: string "Buy" or "Sell".
3471        :param inputParameters: this is dict of strings that looks like this
3472               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3473               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3474               "prices" key: one or more prices to open limit-orders
3475               Counts of values in lots and prices lists must be equals!
3476        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3477        """
3478        # TODO: update order grid work with api v2
3479        pass
3480        # uLogger.debug("Input parameters: {}".format(inputParameters))
3481        #
3482        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3483        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3484        #     raise Exception("Incorrect value")
3485        #
3486        # if "l" in inputParameters.keys():
3487        #     inputParameters["lots"] = inputParameters.pop("l")
3488        #
3489        # if "p" in inputParameters.keys():
3490        #     inputParameters["prices"] = inputParameters.pop("p")
3491        #
3492        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3493        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3494        #     raise Exception("Incorrect value")
3495        #
3496        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3497        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3498        #
3499        # if len(lots) != len(prices):
3500        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3501        #     raise Exception("Incorrect value")
3502        #
3503        # uLogger.debug("Extracted parameters for orders:")
3504        # uLogger.debug("lots = {}".format(lots))
3505        # uLogger.debug("prices = {}".format(prices))
3506        #
3507        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3508        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3509        # uLogger.debug("Order parameters: {}".format(result))
3510        #
3511        # return result
3512
3513    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3514        """
3515        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3516
3517        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3518        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3519        """
3520        result = False
3521        msg = "Instrument not defined!"
3522
3523        if portfolio is None or not portfolio:
3524            portfolio = self.Overview(show=False)
3525
3526        if self.ticker:
3527            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3528            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3529
3530            for iType in TKS_INSTRUMENTS:
3531                for instrument in portfolio["stat"][iType]:
3532                    if instrument["ticker"] == self.ticker:
3533                        result = True
3534                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3535                        break
3536
3537        elif self.figi:
3538            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3539            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3540
3541            for iType in TKS_INSTRUMENTS:
3542                for instrument in portfolio["stat"][iType]:
3543                    if instrument["figi"] == self.figi:
3544                        result = True
3545                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3546                        break
3547
3548        else:
3549            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3550
3551        uLogger.debug(msg)
3552
3553        return result
3554
3555    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3556        """
3557        Returns instrument is in the user's portfolio if it presents there.
3558        Instrument must be defined by `ticker` (highly priority) or `figi`.
3559
3560        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3561        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3562        """
3563        result = None
3564        msg = "Instrument not defined!"
3565
3566        if portfolio is None or not portfolio:
3567            portfolio = self.Overview(show=False)
3568
3569        if self.ticker:
3570            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3571            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3572
3573            for iType in TKS_INSTRUMENTS:
3574                for instrument in portfolio["stat"][iType]:
3575                    if instrument["ticker"] == self.ticker:
3576                        result = instrument
3577                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3578                        break
3579
3580        elif self.figi:
3581            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3582            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3583
3584            for iType in TKS_INSTRUMENTS:
3585                for instrument in portfolio["stat"][iType]:
3586                    if instrument["figi"] == self.figi:
3587                        result = instrument
3588                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3589                        break
3590
3591        else:
3592            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3593
3594        uLogger.debug(msg)
3595
3596        return result
3597
3598    def RequestLimits(self) -> dict:
3599        """
3600        Method for obtaining the available funds for withdrawal for current `accountId`.
3601
3602        See also:
3603        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3604        - `OverviewLimits()` method
3605
3606        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3607                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3608                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3609                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3610        """
3611        if self.accountId is None or not self.accountId:
3612            uLogger.error("Variable `accountId` must be defined for using this method!")
3613            raise Exception("Account ID required")
3614
3615        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3616
3617        self.body = str({"accountId": self.accountId})
3618        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3619        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3620
3621        if self.moreDebug:
3622            uLogger.debug("Records about available funds for withdrawal successfully received")
3623
3624        return rawLimits
3625
3626    def OverviewLimits(self, show: bool = False) -> dict:
3627        """
3628        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3629
3630        See also: `RequestLimits()`.
3631
3632        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3633        :return: dict with raw parsed data from server and some calculated statistics about it.
3634        """
3635        if self.accountId is None or not self.accountId:
3636            uLogger.error("Variable `accountId` must be defined for using this method!")
3637            raise Exception("Account ID required")
3638
3639        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3640
3641        view = {
3642            "rawLimits": rawLimits,
3643            "limits": {  # parsed data for every currency:
3644                "money": {  # this is an array of portfolio currency positions
3645                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3646                },
3647                "blocked": {  # this is an array of blocked currency
3648                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3649                },
3650                "blockedGuarantee": {  # this is locked money under collateral for futures
3651                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3652                },
3653            },
3654        }
3655
3656        # --- Prepare text table with limits in human-readable format:
3657        if show:
3658            info = [
3659                "# Withdrawal limits\n\n",
3660                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3661                "* **Account ID:** [{}]\n".format(self.accountId),
3662            ]
3663
3664            if view["limits"]["money"]:
3665                info.extend([
3666                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3667                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3668                ])
3669
3670            else:
3671                info.append("\nNo withdrawal limits\n")
3672
3673            for curr in view["limits"]["money"].keys():
3674                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3675                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3676                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3677
3678                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3679                    "[{}]".format(curr),
3680                    "{:.2f}".format(view["limits"]["money"][curr]),
3681                    "{:.2f}".format(availableMoney),
3682                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3683                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3684                )
3685
3686                if curr == "rub":
3687                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3688
3689                else:
3690                    info.append(infoStr)
3691
3692            infoText = "".join(info)
3693
3694            uLogger.info(infoText)
3695
3696            if self.withdrawalLimitsFile:
3697                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3698                    fH.write(infoText)
3699
3700                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3701
3702        return view
3703
3704    def RequestAccounts(self) -> dict:
3705        """
3706        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3707
3708        See also:
3709        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3710        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3711        - `OverviewUserInfo()` method
3712
3713        :return: dict with raw data from server that contains accounts info. Example of dict:
3714                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3715                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3716                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3717                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3718        """
3719        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3720
3721        self.body = str({})
3722        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3723        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3724
3725        if self.moreDebug:
3726            uLogger.debug("Records about available accounts successfully received")
3727
3728        return rawAccounts
3729
3730    def RequestUserInfo(self) -> dict:
3731        """
3732        Method for requesting common user's information.
3733
3734        See also:
3735        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3736        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3737        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3738        - `OverviewUserInfo()` method
3739
3740        :return: dict with raw data from server that contains user's information. Example of dict:
3741                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3742                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3743        """
3744        uLogger.debug("Requesting common user's information. Wait, please...")
3745
3746        self.body = str({})
3747        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3748        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3749
3750        if self.moreDebug:
3751            uLogger.debug("Records about current user successfully received")
3752
3753        return rawUserInfo
3754
3755    def RequestMarginStatus(self, accountId: str = None) -> dict:
3756        """
3757        Method for requesting margin calculation for defined account ID.
3758
3759        See also:
3760        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3761        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3762        - `OverviewUserInfo()` method
3763
3764        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3765        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3766                 Example of responses:
3767                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3768                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3769                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3770                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3771                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3772                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3773        """
3774        if accountId is None or not accountId:
3775            if self.accountId is None or not self.accountId:
3776                uLogger.error("Variable `accountId` must be defined for using this method!")
3777                raise Exception("Account ID required")
3778
3779            else:
3780                accountId = self.accountId  # use `self.accountId` (main ID) by default
3781
3782        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3783
3784        self.body = str({"accountId": accountId})
3785        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3786        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3787
3788        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3789            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3790            rawMargin = {}
3791
3792        else:
3793            if self.moreDebug:
3794                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3795
3796        return rawMargin
3797
3798    def RequestTariffLimits(self) -> dict:
3799        """
3800        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3801
3802        See also:
3803        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3804        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3805        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3806        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3807        - `OverviewUserInfo()` method
3808
3809        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3810                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3811                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3812        """
3813        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3814
3815        self.body = str({})
3816        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3817        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3818
3819        if self.moreDebug:
3820            uLogger.debug("Records with limits of current tariff successfully received")
3821
3822        return rawTariffLimits
3823
3824    def RequestBondCoupons(self, iJSON: dict) -> dict:
3825        """
3826        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3827        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3828        All dates are in UTC timezone.
3829
3830        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3831        Documentation:
3832        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3833        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3834
3835        See also: `ExtendBondsData()`.
3836
3837        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3838                      If raw iJSON is not data of bond then server returns an error [400] with message:
3839                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3840        :return: dictionary with bond payment calendar. Response example
3841                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3842                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3843                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3844                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3845        """
3846        if iJSON["figi"] is None or not iJSON["figi"]:
3847            uLogger.error("FIGI must be defined for using this method!")
3848            raise Exception("FIGI required")
3849
3850        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3851        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3852
3853        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3854            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3855            self.figi,
3856            startDate,
3857            endDate,
3858        ))
3859
3860        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3861        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3862        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3863
3864        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3865            uLogger.warning("Instrument type is not bond!")
3866
3867        else:
3868            if self.moreDebug:
3869                uLogger.debug("Records about bond payment calendar successfully received")
3870
3871        return calendar
3872
3873    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3874        """
3875        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3876        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3877        coupon yields, current yields and some statistics etc.
3878
3879        WARNING! This is too long operation if a lot of bonds requested from broker server.
3880
3881        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3882
3883        :param instruments: list of strings with tickers or FIGIs.
3884        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3885                     for further used by data scientists or stock analytics.
3886        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3887                 In XLSX-file and Pandas DataFrame fields mean:
3888                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3889                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3890        """
3891        if instruments is None or not instruments:
3892            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3893            raise Exception("Ticker or FIGI required")
3894
3895        if isinstance(instruments, str):
3896            instruments = [instruments]
3897
3898        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3899
3900        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3901
3902        iCount = len(uniqueInstruments)
3903        tooLong = iCount >= 20
3904        if tooLong:
3905            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3906
3907        bonds = None
3908        for i, self.figi in enumerate(uniqueInstruments):
3909            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3910
3911            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3912                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3913                rawBond = self.SearchByFIGI(requestPrice=True)
3914
3915                # Widen raw data with UTC current time (iData["actualDateTime"]):
3916                actualDate = datetime.now(tzutc())
3917                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3918
3919                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3920                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3921
3922                # Replace some values with human-readable:
3923                iData["nominalCurrency"] = iData["nominal"]["currency"]
3924                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3925                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3926                iData["aciCurrency"] = iData["aciValue"]["currency"]
3927                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3928                iData["issueSize"] = int(iData["issueSize"])
3929                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3930                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3931                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3932                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3933                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3934                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3935                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3936                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3937                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3938                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3939
3940                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3941                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3942                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3943                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3944                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3945                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3946                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3947                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3948                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3949                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3950                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3951
3952                # Widen raw data with calendar data from `rawCalendar` values:
3953                calendarData = []
3954                if "events" in iData["rawCalendar"].keys():
3955                    for item in iData["rawCalendar"]["events"]:
3956                        calendarData.append({
3957                            "couponDate": item["couponDate"],
3958                            "couponNumber": int(item["couponNumber"]),
3959                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3960                            "payCurrency": item["payOneBond"]["currency"],
3961                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3962                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3963                            "couponStartDate": item["couponStartDate"],
3964                            "couponEndDate": item["couponEndDate"],
3965                            "couponPeriod": item["couponPeriod"],
3966                        })
3967
3968                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3969                    if "maturityDate" not in iData.keys():
3970                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3971
3972                # Widen raw data with Coupon Rate.
3973                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3974                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3975                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3976                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3977
3978                # Widen raw data with Yield to Maturity (YTM) on current date.
3979                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3980                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3981                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3982                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3983                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3984                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3985
3986                iData["calendar"] = calendarData  # adds calendar at the end
3987
3988                # Remove not used data:
3989                iData.pop("uid")
3990                iData.pop("positionUid")
3991                iData.pop("currentPrice")
3992                iData.pop("rawCalendar")
3993
3994                colNames = list(iData.keys())
3995                if bonds is None:
3996                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3997
3998                else:
3999                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4000
4001            else:
4002                uLogger.warning("Instrument is not a bond!")
4003
4004            processed = round(100 * (i + 1) / iCount, 1)
4005            if tooLong and processed % 5 == 0:
4006                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4007
4008            else:
4009                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4010
4011        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4012
4013        # Saving bonds from Pandas DataFrame to XLSX sheet:
4014        if xlsx and self.bondsXLSXFile:
4015            with pd.ExcelWriter(
4016                    path=self.bondsXLSXFile,
4017                    date_format=TKS_DATE_FORMAT,
4018                    datetime_format=TKS_DATE_TIME_FORMAT,
4019                    mode="w",
4020            ) as writer:
4021                bonds.to_excel(
4022                    writer,
4023                    sheet_name="Extended bonds data",
4024                    index=True,
4025                    encoding="UTF-8",
4026                    freeze_panes=(1, 1),
4027                )  # saving as XLSX-file with freeze first row and column as headers
4028
4029            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4030
4031        return bonds
4032
4033    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4034        """
4035        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4036
4037        WARNING! This is too long operation if a lot of bonds requested from broker server.
4038
4039        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4040
4041        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4042                        extended information about bonds: main info, current prices, bond payment calendar,
4043                        coupon yields, current yields and some statistics etc.
4044                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4045        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4046                     for further used by data scientists or stock analytics.
4047        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4048        """
4049        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4050            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4051
4052        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4053
4054        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4055        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4056        calendar = None
4057        for bond in extBonds.iterrows():
4058            for item in bond[1]["calendar"]:
4059                cData = {
4060                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4061                    "couponDate": item["couponDate"],
4062                    "figi": bond[1]["figi"],
4063                    "ticker": bond[1]["ticker"],
4064                    "name": bond[1]["name"],
4065                    "couponNumber": item["couponNumber"],
4066                    "payOneBond": item["payOneBond"],
4067                    "payCurrency": item["payCurrency"],
4068                    "couponType": item["couponType"],
4069                    "couponPeriod": item["couponPeriod"],
4070                    "fixDate": item["fixDate"],
4071                    "couponStartDate": item["couponStartDate"],
4072                    "couponEndDate": item["couponEndDate"],
4073                }
4074
4075                if calendar is None:
4076                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4077
4078                else:
4079                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4080
4081        if calendar is not None:
4082            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4083
4084            # Saving calendar from Pandas DataFrame to XLSX sheet:
4085            if xlsx:
4086                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4087
4088                with pd.ExcelWriter(
4089                        path=xlsxCalendarFile,
4090                        date_format=TKS_DATE_FORMAT,
4091                        datetime_format=TKS_DATE_TIME_FORMAT,
4092                        mode="w",
4093                ) as writer:
4094                    humanReadable = calendar.copy(deep=True)
4095                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4096                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4097                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4098                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4099                    humanReadable.columns = colNames  # human-readable column names
4100
4101                    humanReadable.to_excel(
4102                        writer,
4103                        sheet_name="Bond payments calendar",
4104                        index=False,
4105                        encoding="UTF-8",
4106                        freeze_panes=(1, 2),
4107                    )  # saving as XLSX-file with freeze first row and column as headers
4108
4109                    del humanReadable  # release df in memory
4110
4111                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4112
4113        return calendar
4114
4115    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4116        """
4117        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4118        Also, creates Markdown file with calendar data, `calendar.md` by default.
4119
4120        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4121
4122        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4123                        extended information about bonds: main info, current prices, bond payment calendar,
4124                        coupon yields, current yields and some statistics etc.
4125                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4126        :param show: if `True` then also printing bonds payment calendar to the console,
4127                     otherwise save to file `calendarFile` only. `False` by default.
4128        :return: multilines text in Markdown format with bonds payment calendar as a table.
4129        """
4130        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4131            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4132
4133        infoText = "# Bond payments calendar\n\n"
4134
4135        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4136
4137        if not (calendar is None or calendar.empty):
4138            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4139
4140            info = [
4141                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4142                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4143            ]
4144
4145            newMonth = False
4146            notOneBond = calendar["figi"].nunique() > 1
4147            for i, bond in enumerate(calendar.iterrows()):
4148                if newMonth and notOneBond:
4149                    info.append(splitLine)
4150
4151                info.append(
4152                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4153                        "  √" if bond[1]["paid"] else "  —",
4154                        bond[1]["couponDate"].split("T")[0],
4155                        bond[1]["figi"],
4156                        bond[1]["ticker"],
4157                        bond[1]["couponNumber"],
4158                        "{} {}".format(
4159                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4160                            bond[1]["payCurrency"],
4161                        ),
4162                        bond[1]["couponType"],
4163                        bond[1]["couponPeriod"],
4164                        bond[1]["fixDate"].split("T")[0],
4165                    )
4166                )
4167
4168                if i < len(calendar.values) - 1:
4169                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4170                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4171                    newMonth = False if curDate.month == nextDate.month else True
4172
4173                else:
4174                    newMonth = False
4175
4176            infoText += "".join(info)
4177
4178            if show:
4179                uLogger.info("{}".format(infoText))
4180
4181            if self.calendarFile is not None:
4182                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4183                    fH.write(infoText)
4184
4185                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4186
4187        else:
4188            infoText += "No data\n"
4189
4190        return infoText
4191
4192    def OverviewAccounts(self, show: bool = False) -> dict:
4193        """
4194        Method for parsing and show simple table with all available user accounts.
4195
4196        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4197
4198        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4199        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4200                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4201                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4202                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4203                                                        "closed": "—", "access": "Full access" }, ...}}`
4204        """
4205        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4206
4207        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4208        accounts = {
4209            item["id"]: {
4210                "type": TKS_ACCOUNT_TYPES[item["type"]],
4211                "name": item["name"],
4212                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4213                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4214                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4215                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4216            } for item in rawAccounts["accounts"]
4217        }
4218
4219        # Raw and parsed data with some fields replaced in "stat" section:
4220        view = {
4221            "rawAccounts": rawAccounts,
4222            "stat": accounts,
4223        }
4224
4225        # --- Prepare simple text table with only accounts data in human-readable format:
4226        if show:
4227            info = [
4228                "# User accounts\n\n",
4229                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4230                "| Account ID   | Type                      | Status                    | Name                           |\n",
4231                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4232            ]
4233
4234            for account in view["stat"].keys():
4235                info.extend([
4236                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4237                        account,
4238                        view["stat"][account]["type"],
4239                        view["stat"][account]["status"],
4240                        view["stat"][account]["name"],
4241                    )
4242                ])
4243
4244            infoText = "".join(info)
4245
4246            uLogger.info(infoText)
4247
4248            if self.userAccountsFile:
4249                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4250                    fH.write(infoText)
4251
4252                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4253
4254        return view
4255
4256    def OverviewUserInfo(self, show: bool = False) -> dict:
4257        """
4258        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4259
4260        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4261
4262        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4263        :return: dict with raw parsed data from server and some calculated statistics about it.
4264        """
4265        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4266        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4267        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4268        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4269        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4270        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4271
4272        # This is dict with parsed common user data:
4273        userInfo = {
4274            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4275            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4276            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4277            "tariff": rawUserInfo["tariff"],
4278        }
4279
4280        # This is an array of dict with parsed margin statuses for every account IDs:
4281        margins = {}
4282        for accountId in accounts.keys():
4283            if rawMargins[accountId]:
4284                margins[accountId] = {
4285                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4286                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4287                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4288                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4289                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4290                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4291                }
4292
4293            else:
4294                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4295
4296        unary = {}  # unary-connection limits
4297        for item in rawTariffLimits["unaryLimits"]:
4298            if item["limitPerMinute"] in unary.keys():
4299                unary[item["limitPerMinute"]].extend(item["methods"])
4300
4301            else:
4302                unary[item["limitPerMinute"]] = item["methods"]
4303
4304        stream = {}  # stream-connection limits
4305        for item in rawTariffLimits["streamLimits"]:
4306            if item["limit"] in stream.keys():
4307                stream[item["limit"]].extend(item["streams"])
4308
4309            else:
4310                stream[item["limit"]] = item["streams"]
4311
4312        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4313        limits = {
4314            "unary": unary,
4315            "stream": stream,
4316        }
4317
4318        # Raw and parsed data as an output result:
4319        view = {
4320            "rawUserInfo": rawUserInfo,
4321            "rawAccounts": rawAccounts,
4322            "rawMargins": rawMargins,
4323            "rawTariffLimits": rawTariffLimits,
4324            "stat": {
4325                "userInfo": userInfo,
4326                "accounts": accounts,
4327                "margins": margins,
4328                "limits": limits,
4329            },
4330        }
4331
4332        # --- Prepare text table with user information in human-readable format:
4333        if show:
4334            info = [
4335                "# Full user information\n\n",
4336                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4337                "## Common information\n\n",
4338                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4339                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4340                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4341                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4342                "\n## User accounts\n\n",
4343            ]
4344
4345            for account in view["stat"]["accounts"].keys():
4346                info.extend([
4347                    "### ID: [{}]\n\n".format(account),
4348                    "| Parameters           | Values                                                       |\n",
4349                    "|----------------------|--------------------------------------------------------------|\n",
4350                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4351                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4352                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4353                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4354                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4355                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4356                ])
4357
4358                if margins[account]:
4359                    info.extend([
4360                        "| Margin status:       | Enabled                                                      |\n",
4361                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4362                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4363                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4364                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4365                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4366                    ])
4367
4368                else:
4369                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4370
4371            info.extend([
4372                "\n## Current user tariff limits\n",
4373                "\nSee also:\n",
4374                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4375                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4376                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4377                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4378                "\n### Unary limits\n",
4379            ])
4380
4381            if unary:
4382                for key, values in sorted(unary.items()):
4383                    info.append("\n* Max requests per minute: {}\n".format(key))
4384
4385                    for value in values:
4386                        info.append("  - {}\n".format(value))
4387
4388            else:
4389                info.append("\nNot available\n")
4390
4391            info.append("\n### Stream limits\n")
4392
4393            if stream:
4394                for key, values in sorted(stream.items()):
4395                    info.append("\n* Max stream connections: {}\n".format(key))
4396
4397                    for value in values:
4398                        info.append("  - {}\n".format(value))
4399
4400            else:
4401                info.append("\nNot available\n")
4402
4403            infoText = "".join(info)
4404
4405            uLogger.info(infoText)
4406
4407            if self.userInfoFile:
4408                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4409                    fH.write(infoText)
4410
4411                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4412
4413        return view
4414
4415
4416class Args:
4417    """
4418    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4419    """
4420    def __init__(self, **kwargs):
4421        self.__dict__.update(kwargs)
4422
4423    def __getattr__(self, item):
4424        return None
4425
4426
4427def ParseArgs():
4428    """This function get and parse command line keys."""
4429    parser = ArgumentParser()  # command-line string parser
4430
4431    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4432    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4433
4434    # --- options:
4435
4436    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4437    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4438    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4439
4440    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4441    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4442
4443    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4444    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4445
4446    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4447
4448    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4449    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4450    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4451
4452    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4453    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4454
4455    # --- commands:
4456
4457    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4458
4459    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4460    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4461    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4462    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4463    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4464    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4465    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4466    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4467
4468    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4469    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4470    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4471    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4472    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4473
4474    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4475    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4476    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4477    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4478
4479    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4480    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4481    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4482
4483    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4484    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4485    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4486    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4487    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4488    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4489    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4490
4491    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4492    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4493    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4494    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4495    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4496
4497    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4498    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4499    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4500
4501    cmdArgs = parser.parse_args()
4502    return cmdArgs
4503
4504
4505def Main(**kwargs):
4506    """
4507    Main function for work with TKSBrokerAPI in the console.
4508
4509    See examples:
4510    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4511    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4512    """
4513    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4514
4515    if args.debug_level:
4516        uLogger.level = 10  # always debug level by default
4517        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4518
4519    exitCode = 0
4520    start = datetime.now(tzutc())
4521    uLogger.debug("=-" * 50)
4522    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4523        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4524        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4525    ))
4526
4527    # trying to calculate full current version:
4528    buildVersion = __version__
4529    try:
4530        v = version("tksbrokerapi")
4531        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4532
4533    except Exception:
4534        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4535
4536    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4537    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4538
4539    try:
4540        if args.version:
4541            print("TKSBrokerAPI {}".format(buildVersion))
4542            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4543
4544        else:
4545            # Init class for trading with Tinkoff Broker:
4546            trader = TinkoffBrokerServer(
4547                token=args.token,
4548                accountId=args.account_id,
4549                useCache=not args.no_cache,
4550            )
4551
4552            # --- set some options:
4553
4554            if args.more:
4555                trader.moreDebug = True
4556                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4557
4558            if args.ticker:
4559                if args.ticker in trader.aliasesKeys:
4560                    trader.ticker = trader.aliases[args.ticker]  # Replace some tickers with its aliases
4561
4562                else:
4563                    trader.ticker = args.ticker
4564
4565            if args.figi:
4566                trader.figi = args.figi
4567
4568            if args.depth is not None:
4569                trader.depth = args.depth
4570
4571            # --- do one command:
4572
4573            if args.list:
4574                if args.output is not None:
4575                    trader.instrumentsFile = args.output
4576
4577                trader.ShowInstrumentsInfo(show=True)
4578
4579            elif args.list_xlsx:
4580                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4581
4582            elif args.bonds_xlsx is not None:
4583                if args.output is not None:
4584                    trader.bondsXLSXFile = args.output
4585
4586                if len(args.bonds_xlsx) == 0:
4587                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4588
4589                else:
4590                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4591
4592            elif args.search:
4593                if args.output is not None:
4594                    trader.searchResultsFile = args.output
4595
4596                trader.SearchInstruments(pattern=args.search[0], show=True)
4597
4598            elif args.info:
4599                if not (args.ticker or args.figi):
4600                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4601                    raise Exception("Ticker or FIGI required")
4602
4603                if args.output is not None:
4604                    trader.infoFile = args.output
4605
4606                if args.ticker:
4607                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4608
4609                else:
4610                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4611
4612            elif args.calendar is not None:
4613                if args.output is not None:
4614                    trader.calendarFile = args.output
4615
4616                if len(args.calendar) == 0:
4617                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4618
4619                else:
4620                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4621
4622                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4623
4624            elif args.price:
4625                if not (args.ticker or args.figi):
4626                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4627                    raise Exception("Ticker or FIGI required")
4628
4629                trader.GetCurrentPrices(show=True)
4630
4631            elif args.prices is not None:
4632                if args.output is not None:
4633                    trader.pricesFile = args.output
4634
4635                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4636
4637            elif args.overview:
4638                if args.output is not None:
4639                    trader.overviewFile = args.output
4640
4641                trader.Overview(show=True, details="full")
4642
4643            elif args.overview_digest:
4644                if args.output is not None:
4645                    trader.overviewDigestFile = args.output
4646
4647                trader.Overview(show=True, details="digest")
4648
4649            elif args.overview_positions:
4650                if args.output is not None:
4651                    trader.overviewPositionsFile = args.output
4652
4653                trader.Overview(show=True, details="positions")
4654
4655            elif args.overview_orders:
4656                if args.output is not None:
4657                    trader.overviewOrdersFile = args.output
4658
4659                trader.Overview(show=True, details="orders")
4660
4661            elif args.overview_analytics:
4662                if args.output is not None:
4663                    trader.overviewAnalyticsFile = args.output
4664
4665                trader.Overview(show=True, details="analytics")
4666
4667            elif args.deals is not None:
4668                if args.output is not None:
4669                    trader.reportFile = args.output
4670
4671                if 0 <= len(args.deals) < 3:
4672                    trader.Deals(
4673                        start=args.deals[0] if len(args.deals) >= 1 else None,
4674                        end=args.deals[1] if len(args.deals) == 2 else None,
4675                        show=True,  # Always show deals report in console
4676                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4677                    )
4678
4679                else:
4680                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4681                    raise Exception("Incorrect value")
4682
4683            elif args.history is not None:
4684                if args.output is not None:
4685                    trader.historyFile = args.output
4686
4687                if 0 <= len(args.history) < 3:
4688                    dataReceived = trader.History(
4689                        start=args.history[0] if len(args.history) >= 1 else None,
4690                        end=args.history[1] if len(args.history) == 2 else None,
4691                        interval="hour" if args.interval is None or not args.interval else args.interval,
4692                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4693                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4694                        show=True,  # shows all downloaded candles in console
4695                    )
4696
4697                    if args.render_chart is not None and dataReceived is not None:
4698                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4699
4700                        trader.ShowHistoryChart(
4701                            candles=dataReceived,
4702                            interact=iChart,
4703                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4704                        )
4705
4706                else:
4707                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4708                    raise Exception("Incorrect value")
4709
4710            elif args.load_history is not None:
4711                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4712
4713                if args.render_chart is not None and histData is not None:
4714                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4715                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4716
4717                    trader.ShowHistoryChart(
4718                        candles=histData,
4719                        interact=iChart,
4720                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4721                    )
4722
4723            elif args.trade is not None:
4724                if 1 <= len(args.trade) <= 5:
4725                    trader.Trade(
4726                        operation=args.trade[0],
4727                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4728                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4729                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4730                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4731                    )
4732
4733                else:
4734                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4735
4736            elif args.buy is not None:
4737                if 0 <= len(args.buy) <= 4:
4738                    trader.Buy(
4739                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4740                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4741                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4742                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4743                    )
4744
4745                else:
4746                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4747
4748            elif args.sell is not None:
4749                if 0 <= len(args.sell) <= 4:
4750                    trader.Sell(
4751                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4752                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4753                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4754                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4755                    )
4756
4757                else:
4758                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4759
4760            elif args.order:
4761                if 4 <= len(args.order) <= 7:
4762                    trader.Order(
4763                        operation=args.order[0],
4764                        orderType=args.order[1],
4765                        lots=int(args.order[2]),
4766                        targetPrice=float(args.order[3]),
4767                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4768                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4769                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4770                    )
4771
4772                else:
4773                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4774
4775            elif args.buy_limit:
4776                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4777
4778            elif args.sell_limit:
4779                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4780
4781            elif args.buy_stop:
4782                if 2 <= len(args.buy_stop) <= 7:
4783                    trader.BuyStop(
4784                        lots=int(args.buy_stop[0]),
4785                        targetPrice=float(args.buy_stop[1]),
4786                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4787                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4788                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4789                    )
4790
4791                else:
4792                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4793
4794            elif args.sell_stop:
4795                if 2 <= len(args.sell_stop) <= 7:
4796                    trader.SellStop(
4797                        lots=int(args.sell_stop[0]),
4798                        targetPrice=float(args.sell_stop[1]),
4799                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4800                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4801                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4802                    )
4803
4804                else:
4805                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4806
4807            # elif args.buy_order_grid is not None:
4808            #     # update order grid work with api v2
4809            #     if len(args.buy_order_grid) == 2:
4810            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4811            #
4812            #         for order in orderParams:
4813            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4814            #
4815            #     else:
4816            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4817            #
4818            # elif args.sell_order_grid is not None:
4819            #     # update order grid work with api v2
4820            #     if len(args.sell_order_grid) >= 2:
4821            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4822            #
4823            #         for order in orderParams:
4824            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4825            #
4826            #     else:
4827            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4828
4829            elif args.close_order is not None:
4830                trader.CloseOrders(args.close_order)  # close only one order
4831
4832            elif args.close_orders is not None:
4833                trader.CloseOrders(args.close_orders)  # close list of orders
4834
4835            elif args.close_trade:
4836                if not (args.ticker or args.figi):
4837                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4838                    raise Exception("Ticker or FIGI required")
4839
4840                if args.ticker:
4841                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4842
4843                else:
4844                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4845
4846            elif args.close_trades is not None:
4847                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4848
4849            elif args.close_all is not None:
4850                trader.CloseAll(*args.close_all)
4851
4852            elif args.limits:
4853                if args.output is not None:
4854                    trader.withdrawalLimitsFile = args.output
4855
4856                trader.OverviewLimits(show=True)
4857
4858            elif args.user_info:
4859                if args.output is not None:
4860                    trader.userInfoFile = args.output
4861
4862                trader.OverviewUserInfo(show=True)
4863
4864            elif args.account:
4865                if args.output is not None:
4866                    trader.userAccountsFile = args.output
4867
4868                trader.OverviewAccounts(show=True)
4869
4870            else:
4871                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4872                raise Exception("There is no command to execute")
4873
4874    except Exception:
4875        trace = tb.format_exc()
4876        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4877            if e in trace:
4878                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4879                break
4880
4881        uLogger.debug(trace)
4882        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4883        exitCode = 255  # an error occurred, must be open a ticket for this issue
4884
4885    finally:
4886        finish = datetime.now(tzutc())
4887
4888        if exitCode == 0:
4889            if args.more:
4890                uLogger.debug("All operations were finished success (summary code is 0).")
4891
4892        else:
4893            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4894                os.path.abspath(uLog.defaultLogFile), exitCode,
4895            ))
4896
4897        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4898        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4899            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4900            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4901        ))
4902        uLogger.debug("=-" * 50)
4903
4904        if not kwargs:
4905            sys.exit(exitCode)
4906
4907        else:
4908            return exitCode
4909
4910
4911if __name__ == "__main__":
4912    Main()
def NanoToFloat(units: str, nano: int) -> float:
80def NanoToFloat(units: str, nano: int) -> float:
81    """
82    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
83
84    `NanoToFloat(units="2", nano=500000000) -> 2.5`
85
86    `NanoToFloat(units="0", nano=50000000) -> 0.05`
87
88    :param units: integer string or integer parameter that represents the integer part of number
89    :param nano: integer string or integer parameter that represents the fractional part of number
90    :return: float view of number
91    """
92    return int(units) + int(nano) * NANO

Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:

NanoToFloat(units="2", nano=500000000) -> 2.5

NanoToFloat(units="0", nano=50000000) -> 0.05

Parameters
  • units: integer string or integer parameter that represents the integer part of number
  • nano: integer string or integer parameter that represents the fractional part of number
Returns

float view of number

def FloatToNano(number: float) -> dict:
 95def FloatToNano(number: float) -> dict:
 96    """
 97    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
 98
 99    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
100
101    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
102
103    :param number: float number
104    :return: nano-type view of number: `{"units": "string", "nano": integer}`
105    """
106    splitByPoint = str(number).split(".")
107    frac = 0
108
109    if len(splitByPoint) > 1:
110        if len(splitByPoint[1]) <= 9:
111            frac = int("{}{}".format(
112                int(splitByPoint[1]),
113                "0" * (9 - len(splitByPoint[1])),
114            ))
115
116    if (number < 0) and (frac > 0):
117        frac = -frac
118
119    return {"units": str(int(number)), "nano": frac}

Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:

FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}

FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}

Parameters
  • number: float number
Returns

nano-type view of number: {"units": "string", "nano": integer}

def GetDatesAsString(start: str = None, end: str = None) -> tuple:
122def GetDatesAsString(start: str = None, end: str = None) -> tuple:
123    """
124    Create tuple of date and time strings with timezone parsed from user-friendly date.
125
126    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
127
128    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
129    An error exception will occur if input date has incorrect format.
130
131    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
132    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
133    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
134    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
135
136    Also, you can use keywords for start if `end=None`:
137    `today` (from 00:00:00 to the end of current day),
138    `yesterday` (-1 day from 00:00:00 to 23:59:59),
139    `week` (-7 day from 00:00:00 to the end of current day),
140    `month` (-30 day from 00:00:00 to the end of current day),
141    `year` (-365 day from 00:00:00 to the end of current day),
142
143    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
144             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
145             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
146    """
147    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
148    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
149    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
150
151    # time between start and the end of the current day:
152    if start is None or start.lower() == "today":
153        pass
154
155    # from start of the last day to the end of the last day:
156    elif start.lower() == "yesterday":
157        s -= timedelta(days=1)
158        e -= timedelta(days=1)
159
160    # week (-7 day from 00:00:00 to the end of the current day):
161    elif start.lower() == "week":
162        s -= timedelta(days=6)  # +1 current day already taken into account
163
164    # month (-30 day from 00:00:00 to the end of current day):
165    elif start.lower() == "month":
166        s -= timedelta(days=29)  # +1 current day already taken into account
167
168    # year (-365 day from 00:00:00 to the end of current day):
169    elif start.lower() == "year":
170        s -= timedelta(days=364)  # +1 current day already taken into account
171
172    # -N days ago to the end of current day:
173    elif start.startswith('-') and start[1:].isdigit():
174        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
175
176    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
177    else:
178        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
179        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
180
181    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
182    s = s.strftime(TKS_DATE_TIME_FORMAT)
183    e = e.strftime(TKS_DATE_TIME_FORMAT)
184
185    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
186
187    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 - how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 190class TinkoffBrokerServer:
 191    """
 192    This class implements methods to work with Tinkoff broker server.
 193
 194    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 195
 196    About `token`: https://tinkoff.github.io/investAPI/token/
 197    """
 198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 199        """
 200        Main class init.
 201
 202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 205        :param useCache: use default cache file with raw data to use instead of `iList`.
 206                         True by default. Cache is auto-update if new day has come.
 207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 208        :param defaultCache: path to default cache file. `dump.json` by default.
 209        """
 210        if token is None or not token:
 211            try:
 212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 214
 215            except KeyError:
 216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 217                raise Exception("Token required")
 218
 219        else:
 220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 222
 223        if accountId is None or not accountId:
 224            try:
 225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 227
 228            except KeyError:
 229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 230
 231        else:
 232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 234
 235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 237
 238        Latest version: https://pypi.org/project/tksbrokerapi/
 239        """
 240
 241        self.aliases = TKS_TICKER_ALIASES
 242        """Some aliases instead official tickers.
 243
 244        See also: `TKSEnums.TKS_TICKER_ALIASES`
 245        """
 246
 247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 248
 249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 250
 251        self.ticker = ""
 252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 253
 254        See also: `SearchByTicker()`, `SearchInstruments()`.
 255        """
 256
 257        self.figi = ""
 258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 259
 260        See also: `SearchByFIGI()`, `SearchInstruments()`.
 261        """
 262
 263        self.depth = 1
 264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 265
 266        See also: `GetCurrentPrices()`.
 267        """
 268
 269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 271
 272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 273        """
 274
 275        uLogger.debug("Broker API server: {}".format(self.server))
 276
 277        self.timeout = 15
 278        """Server operations timeout in seconds. Default: `15`.
 279
 280        See also: `SendAPIRequest()`.
 281        """
 282
 283        self.headers = {
 284            "Content-Type": "application/json",
 285            "accept": "application/json",
 286            "Authorization": "Bearer {}".format(self.token),
 287            "x-app-name": "Tim55667757.TKSBrokerAPI",
 288        }
 289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 290
 291        See also: `SendAPIRequest()`.
 292        """
 293
 294        self.body = None
 295        """Request body which send to broker server. Default: `None`.
 296
 297        See also: `SendAPIRequest()`.
 298        """
 299
 300        self.moreDebug = False
 301        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 302
 303        self.historyFile = None
 304        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 305
 306        See also: `History()`.
 307        """
 308
 309        self.htmlHistoryFile = "index.html"
 310        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 311
 312        See also: `ShowHistoryChart()`.
 313        """
 314
 315        self.instrumentsFile = "instruments.md"
 316        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 317
 318        See also: `ShowInstrumentsInfo()`.
 319        """
 320
 321        self.searchResultsFile = "search-results.md"
 322        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 323
 324        See also: `SearchInstruments()`.
 325        """
 326
 327        self.pricesFile = "prices.md"
 328        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 329
 330        See also: `GetListOfPrices()`.
 331        """
 332
 333        self.infoFile = "info.md"
 334        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 335
 336        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 337        """
 338
 339        self.bondsXLSXFile = "ext-bonds.xlsx"
 340        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 341        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 342
 343        See also: `ExtendBondsData()`.
 344        """
 345
 346        self.calendarFile = "calendar.md"
 347        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 348        
 349        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 350
 351        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 352        """
 353
 354        self.overviewFile = "overview.md"
 355        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 356
 357        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 358        """
 359
 360        self.overviewDigestFile = "overview-digest.md"
 361        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 362
 363        See also: `Overview()` with parameter `details="digest"`.
 364        """
 365
 366        self.overviewPositionsFile = "overview-positions.md"
 367        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 368
 369        See also: `Overview()` with parameter `details="positions"`.
 370        """
 371
 372        self.overviewOrdersFile = "overview-orders.md"
 373        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 374
 375        See also: `Overview()` with parameter `details="orders"`.
 376        """
 377
 378        self.overviewAnalyticsFile = "overview-analytics.md"
 379        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 380
 381        See also: `Overview()` with parameter `details="analytics"`.
 382        """
 383
 384        self.reportFile = "deals.md"
 385        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 386
 387        See also: `Deals()`.
 388        """
 389
 390        self.withdrawalLimitsFile = "limits.md"
 391        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 392
 393        See also: `OverviewLimits()` and `RequestLimits()`.
 394        """
 395
 396        self.userInfoFile = "user-info.md"
 397        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 398
 399        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 400        """
 401
 402        self.userAccountsFile = "accounts.md"
 403        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 404
 405        See also: `OverviewAccounts()`, `RequestAccounts()`.
 406        """
 407
 408        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 409        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 410
 411        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 412
 413        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 414        """
 415
 416        self.iList = None  # init iList for raw instruments data
 417        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 418        
 419        See also: `Listing()`, `DumpInstruments()`.
 420        """
 421
 422        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 423        if useCache:
 424            if os.path.exists(self.iListDumpFile):
 425                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 426                curTime = datetime.now(tzutc())
 427
 428                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 429                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 430
 431                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 432
 433                else:
 434                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 435
 436                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 437                        os.path.abspath(self.iListDumpFile),
 438                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 439                    ))
 440
 441            else:
 442                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 443                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 444
 445        else:
 446            self.iList = self.Listing()  # request new raw instruments data from broker server
 447            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 448
 449        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 450        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 451
 452        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 453        """
 454
 455    def _ParseJSON(self, rawData="{}") -> dict:
 456        """
 457        Parse JSON from response string.
 458
 459        :param rawData: this is a string with JSON-formatted text.
 460        :return: JSON (dictionary), parsed from server response string.
 461        """
 462        responseJSON = json.loads(rawData) if rawData else {}
 463
 464        if self.moreDebug:
 465            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 466
 467        return responseJSON
 468
 469    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 470        """
 471        Send GET or POST request to broker server and receive JSON object.
 472
 473        self.header: must be defining with dictionary of headers.
 474        self.body: if define then used as request body. None by default.
 475        self.timeout: global request timeout, 15 seconds by default.
 476        :param url: url with REST request.
 477        :param reqType: send "GET" or "POST" request. "GET" by default.
 478        :param retry: how many times retry after first request if an 5xx server errors occurred.
 479        :param pause: sleep time in seconds between retries.
 480        :return: response JSON (dictionary) from broker.
 481        """
 482        if reqType not in ("GET", "POST"):
 483            uLogger.error("You can define request type: 'GET' or 'POST'!")
 484            raise Exception("Incorrect value")
 485
 486        if self.moreDebug:
 487            uLogger.debug("Request parameters:")
 488            uLogger.debug("    - REST API URL: {}".format(url))
 489            uLogger.debug("    - request type: {}".format(reqType))
 490            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 491            uLogger.debug("    - body:\n{}".format(self.body))
 492
 493        # fast hack to avoid all operations with some tickers/FIGI
 494        responseJSON = {}
 495        oK = True
 496        for item in self.exclude:
 497            if item in url:
 498                if self.moreDebug:
 499                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 500
 501                oK = False
 502                break
 503
 504        if oK:
 505            counter = 0
 506            response = None
 507            errMsg = ""
 508
 509            while not response and counter <= retry:
 510                if reqType == "GET":
 511                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 512
 513                if reqType == "POST":
 514                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 515
 516                if self.moreDebug:
 517                    uLogger.debug("Response:")
 518                    uLogger.debug("    - status code: {}".format(response.status_code))
 519                    uLogger.debug("    - reason: {}".format(response.reason))
 520                    uLogger.debug("    - body length: {}".format(len(response.text)))
 521                    uLogger.debug("    - headers:\n{}".format(response.headers))
 522
 523                # Server returns some headers:
 524                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 525                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 526                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 527                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 528                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 529                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 530                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 531                    sleep(rateLimitWait)
 532
 533                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 534                if 400 <= response.status_code < 500:
 535                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 536                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 537                    counter = retry + 1
 538
 539                if 500 <= response.status_code < 600:
 540                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 541                    uLogger.debug("    - not oK, {}".format(errMsg))
 542                    counter += 1
 543
 544                    if counter <= retry:
 545                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 546                        sleep(pause)
 547
 548            responseJSON = self._ParseJSON(rawData=response.text)
 549
 550            if errMsg:
 551                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 552                uLogger.error("    - not oK, {}".format(errMsg))
 553
 554        return responseJSON
 555
 556    def _IUpdater(self, iType: str) -> tuple:
 557        """
 558        Request instrument by type from server. See available API methods for instruments:
 559        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 560        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 561        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 562        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 563        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 564
 565        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 566        :return: tuple with iType name and list of available instruments of current type for defined user token.
 567        """
 568        result = []
 569
 570        if iType in TKS_INSTRUMENTS:
 571            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 572
 573            # all instruments have the same body in API v2 requests:
 574            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 575            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 576            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 577
 578        return iType, result
 579
 580    def _IWrapper(self, kwargs):
 581        """
 582        Wrapper runs instrument's update method `_IUpdater()`.
 583        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 584        """
 585        return self._IUpdater(**kwargs)
 586
 587    def Listing(self) -> dict:
 588        """
 589        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 590
 591        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 592        """
 593        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 594        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 595
 596        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 597        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 598        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 599
 600        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 601        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 602        poolUpdater.close()
 603
 604        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 605        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 606        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 607
 608        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 609        for iType in iList.keys():
 610            for ticker in iList[iType]:
 611                iList[iType][ticker]["type"] = iType
 612
 613                if "minPriceIncrement" in iList[iType][ticker].keys():
 614                    iList[iType][ticker]["step"] = NanoToFloat(
 615                        iList[iType][ticker]["minPriceIncrement"]["units"],
 616                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 617                    )
 618
 619                else:
 620                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 621
 622        return iList
 623
 624    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 625        """
 626        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 627
 628        See also: `DumpInstruments()`, `Listing()`.
 629
 630        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 631                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 632        """
 633        if self.iListDumpFile is None or not self.iListDumpFile:
 634            uLogger.error("Output name of dump file must be defined!")
 635            raise Exception("Filename required")
 636
 637        if not self.iList or forceUpdate:
 638            self.iList = self.Listing()
 639
 640        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 641
 642        # Save as XLSX with separated sheets for every type of instruments:
 643        with pd.ExcelWriter(
 644                path=xlsxDumpFile,
 645                date_format=TKS_DATE_FORMAT,
 646                datetime_format=TKS_DATE_TIME_FORMAT,
 647                mode="w",
 648        ) as writer:
 649            for iType in TKS_INSTRUMENTS:
 650                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 651                df = df[sorted(df)]  # sorted by column names
 652                df = df.applymap(
 653                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 654                    na_action="ignore",
 655                )  # converting numbers from nano-type to float in every cell
 656                df.to_excel(
 657                    writer,
 658                    sheet_name=iType,
 659                    encoding="UTF-8",
 660                    freeze_panes=(1, 1),
 661                )  # saving as XLSX-file with freeze first row and column as headers
 662
 663        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 664
 665    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 666        """
 667        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 668        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 669
 670        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 671
 672        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 673                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 674        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 675        """
 676        if self.iListDumpFile is None or not self.iListDumpFile:
 677            uLogger.error("Output name of dump file must be defined!")
 678            raise Exception("Filename required")
 679
 680        if not self.iList or forceUpdate:
 681            self.iList = self.Listing()
 682
 683        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 684        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 685            fH.write(jsonDump)
 686
 687        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 688
 689        return jsonDump
 690
 691    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 692        """
 693        Show information about one instrument defined by json data and prints it in Markdown format.
 694
 695        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 696
 697        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 698        :param show: if `True` then also printing information about instrument and its current price.
 699        :return: multilines text in Markdown format with information about one instrument.
 700        """
 701        splitLine = "|                                                             |                                                        |\n"
 702        infoText = ""
 703
 704        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 705            info = [
 706                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 707                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 708                "| Parameters                                                  | Values                                                 |\n",
 709                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 710                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 711                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 712            ]
 713
 714            if "sector" in iJSON.keys() and iJSON["sector"]:
 715                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 716
 717            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 718                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 719                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 720            )))
 721
 722            info.extend([
 723                splitLine,
 724                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 725                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 726            ])
 727
 728            if "isin" in iJSON.keys() and iJSON["isin"]:
 729                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 730
 731            if "classCode" in iJSON.keys():
 732                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 733
 734            info.extend([
 735                splitLine,
 736                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 737                splitLine,
 738                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 739                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 740                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 741            ])
 742
 743            if iJSON["figi"]:
 744                self.figi = iJSON["figi"]
 745                iJSON = iJSON | self.RequestTradingStatus()
 746
 747                info.extend([
 748                    splitLine,
 749                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 750                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 751                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 752                ])
 753
 754            info.append(splitLine)
 755
 756            if "type" in iJSON.keys() and iJSON["type"]:
 757                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 758
 759            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 760                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 761
 762            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 763                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 764
 765            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 766                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 767
 768            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 769                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 770
 771            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 772                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 773
 774            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 775                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 776
 777            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 778                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 779
 780            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 781                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 782
 783            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 784                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 785
 786            if "currency" in iJSON.keys():
 787                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 788
 789            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 790                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 791
 792            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 793                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 794
 795            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 796                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 797
 798            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 799                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 800
 801            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 802                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 803
 804            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 805                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 806
 807            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 808                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 809
 810            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 811                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 812
 813            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 814                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 815
 816            iExt = None
 817            if iJSON["type"] == "Bonds":
 818                info.extend([
 819                    splitLine,
 820                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 821                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 822                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 823                        iJSON["nominal"]["currency"],
 824                    )),
 825                ])
 826
 827                if "floatingCouponFlag" in iJSON.keys():
 828                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 829
 830                if "amortizationFlag" in iJSON.keys():
 831                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 832
 833                info.append(splitLine)
 834
 835                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 836                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 837
 838                if iJSON["figi"]:
 839                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 840
 841                    info.extend([
 842                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 843                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 844                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 845                    ])
 846
 847                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 848                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 849                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 850                        iJSON["aciValue"]["currency"]
 851                    )))
 852
 853            if "currentPrice" in iJSON.keys():
 854                info.append(splitLine)
 855
 856                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 857                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 858
 859                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 860                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 861                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 862                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 863                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 864
 865                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 866                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 867
 868                info.extend([
 869                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 870                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 871                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 872                    )),
 873                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 874                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 875                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 876                    )),
 877                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 878                        "{:.2f}%{}".format(
 879                            iJSON["currentPrice"]["changes"],
 880                            " ({}{:.2f} {})".format(
 881                                "+" if bondChangesDelta > 0 else "",
 882                                bondChangesDelta,
 883                                aciCurrency
 884                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 885                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 886                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 887                                currency
 888                            ),
 889                        )
 890                    ),
 891                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 892                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 893                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 894                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 896                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 897                    )),
 898                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 899                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 900                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 901                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 902                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 903                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 904                    )),
 905                ])
 906
 907            if "lot" in iJSON.keys():
 908                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 909
 910            if "step" in iJSON.keys() and iJSON["step"] != 0:
 911                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 912
 913            # Add bond payment calendar:
 914            if iJSON["type"] == "Bonds":
 915                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 916                info.extend(["\n", strCalendar])
 917
 918            infoText += "".join(info)
 919
 920            if show:
 921                uLogger.info("{}".format(infoText))
 922
 923            else:
 924                uLogger.debug("{}".format(infoText))
 925
 926            if self.infoFile is not None:
 927                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 928                    fH.write(infoText)
 929
 930                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 931
 932        return infoText
 933
 934    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 935        """
 936        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 937
 938        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 939        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 940        :return: JSON formatted data with information about instrument.
 941        """
 942        tickerJSON = {}
 943        if self.moreDebug:
 944            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 945
 946        if not self.ticker:
 947            uLogger.warning("self.ticker variable is not be empty!")
 948
 949        else:
 950            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 951                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 952                raise Exception("Instrument not allowed")
 953
 954            if not self.iList:
 955                self.iList = self.Listing()
 956
 957            if self.ticker in self.iList["Shares"].keys():
 958                tickerJSON = self.iList["Shares"][self.ticker]
 959                if self.moreDebug:
 960                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 961
 962            elif self.ticker in self.iList["Currencies"].keys():
 963                tickerJSON = self.iList["Currencies"][self.ticker]
 964                if self.moreDebug:
 965                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 966
 967            elif self.ticker in self.iList["Bonds"].keys():
 968                tickerJSON = self.iList["Bonds"][self.ticker]
 969                if self.moreDebug:
 970                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 971
 972            elif self.ticker in self.iList["Etfs"].keys():
 973                tickerJSON = self.iList["Etfs"][self.ticker]
 974                if self.moreDebug:
 975                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 976
 977            elif self.ticker in self.iList["Futures"].keys():
 978                tickerJSON = self.iList["Futures"][self.ticker]
 979                if self.moreDebug:
 980                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 981
 982        if tickerJSON:
 983            self.figi = tickerJSON["figi"]
 984
 985            if requestPrice:
 986                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 987
 988                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 989                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 990
 991                else:
 992                    tickerJSON["currentPrice"]["changes"] = 0
 993
 994            if show:
 995                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 996
 997        else:
 998            if show:
 999                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1000
1001        return tickerJSON
1002
1003    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1004        """
1005        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1006
1007        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1008        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1009        :return: JSON formatted data with information about instrument.
1010        """
1011        figiJSON = {}
1012        if self.moreDebug:
1013            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1014
1015        if not self.figi:
1016            uLogger.warning("self.figi variable is not be empty!")
1017
1018        else:
1019            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1020                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1021                raise Exception("Instrument not allowed")
1022
1023            if not self.iList:
1024                self.iList = self.Listing()
1025
1026            for item in self.iList["Shares"].keys():
1027                if self.figi == self.iList["Shares"][item]["figi"]:
1028                    figiJSON = self.iList["Shares"][item]
1029
1030                    if self.moreDebug:
1031                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1032
1033                    break
1034
1035            if not figiJSON:
1036                for item in self.iList["Currencies"].keys():
1037                    if self.figi == self.iList["Currencies"][item]["figi"]:
1038                        figiJSON = self.iList["Currencies"][item]
1039
1040                        if self.moreDebug:
1041                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1042
1043                        break
1044
1045            if not figiJSON:
1046                for item in self.iList["Bonds"].keys():
1047                    if self.figi == self.iList["Bonds"][item]["figi"]:
1048                        figiJSON = self.iList["Bonds"][item]
1049
1050                        if self.moreDebug:
1051                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1052
1053                        break
1054
1055            if not figiJSON:
1056                for item in self.iList["Etfs"].keys():
1057                    if self.figi == self.iList["Etfs"][item]["figi"]:
1058                        figiJSON = self.iList["Etfs"][item]
1059
1060                        if self.moreDebug:
1061                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1062
1063                        break
1064
1065            if not figiJSON:
1066                for item in self.iList["Futures"].keys():
1067                    if self.figi == self.iList["Futures"][item]["figi"]:
1068                        figiJSON = self.iList["Futures"][item]
1069
1070                        if self.moreDebug:
1071                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1072
1073                        break
1074
1075        if figiJSON:
1076            self.figi = figiJSON["figi"]
1077            self.ticker = figiJSON["ticker"]
1078
1079            if requestPrice:
1080                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1081
1082                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1083                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1084
1085                else:
1086                    figiJSON["currentPrice"]["changes"] = 0
1087
1088            if show:
1089                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1090
1091        else:
1092            if show:
1093                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1094
1095        return figiJSON
1096
1097    def GetCurrentPrices(self, show: bool = True) -> dict:
1098        """
1099        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1100        `{"buy": [{"price": 1243.8, "quantity": 193},
1101                  {"price": 1244.0, "quantity": 168},
1102                  {"price": 1244.8, "quantity": 5},
1103                  {"price": 1245.0, "quantity": 61},
1104                  {"price": 1245.4, "quantity": 60}],
1105          "sell": [{"price": 1243.6, "quantity": 8},
1106                   {"price": 1242.6, "quantity": 10},
1107                   {"price": 1242.4, "quantity": 18},
1108                   {"price": 1242.2, "quantity": 50},
1109                   {"price": 1242.0, "quantity": 113}],
1110          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1111        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1112        - sell: list of dicts with Buyers prices,
1113            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1114            - quantity: volume value by current price in lots,
1115        - limitUp: current trade session limit price, maximum,
1116        - limitDown: current trade session limit price, minimum,
1117        - lastPrice: last deal price of the instrument,
1118        - closePrice: previous trade session close price of the instrument.
1119
1120        See also: `SearchByTicker()` and `SearchByFIGI()`.
1121        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1122        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1123
1124        :param show: if `True` then print DOM to log and console.
1125        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1126                 If an error occurred then returns an empty record:
1127                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1128        """
1129        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1130
1131        if self.depth < 1:
1132            uLogger.error("Depth of Market (DOM) must be >=1!")
1133            raise Exception("Incorrect value")
1134
1135        if not (self.ticker or self.figi):
1136            uLogger.error("self.ticker or self.figi variables must be defined!")
1137            raise Exception("Ticker or FIGI required")
1138
1139        if self.ticker and not self.figi:
1140            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1141            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1142
1143        if not self.ticker and self.figi:
1144            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1145            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1146
1147        if not self.figi:
1148            uLogger.error("FIGI is not defined!")
1149            raise Exception("Ticker or FIGI required")
1150
1151        else:
1152            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1153
1154            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1155            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1156            self.body = str({"figi": self.figi, "depth": self.depth})
1157            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1158
1159            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1160                # list of dicts with sellers orders:
1161                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1162
1163                # list of dicts with buyers orders:
1164                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1165
1166                # max price of instrument at this time:
1167                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1168
1169                # min price of instrument at this time:
1170                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1171
1172                # last price of deal with instrument:
1173                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1174
1175                # last close price of instrument:
1176                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1177
1178            else:
1179                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1180                uLogger.debug("Server response: {}".format(pricesResponse))
1181
1182            if show:
1183                if prices["buy"] or prices["sell"]:
1184                    info = [
1185                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1186                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1187                            self.ticker,
1188                            self.figi,
1189                            self.depth,
1190                        ),
1191                        "-" * 60, "\n",
1192                        "             Orders of Buyers | Orders of Sellers\n",
1193                        "-" * 60, "\n",
1194                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1195                        "-" * 60, "\n",
1196                    ]
1197
1198                    if not prices["buy"]:
1199                        info.append("                              | No orders!\n")
1200                        sumBuy = 0
1201
1202                    else:
1203                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1204                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1205                        for item in maxMinSorted:
1206                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1207
1208                    if not prices["sell"]:
1209                        info.append("No orders!                    |\n")
1210                        sumSell = 0
1211
1212                    else:
1213                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1214                        for item in prices["sell"]:
1215                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1216
1217                    info.extend([
1218                        "-" * 60, "\n",
1219                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1220                        "-" * 60, "\n",
1221                    ])
1222
1223                    infoText = "".join(info)
1224
1225                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1226
1227                else:
1228                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1229
1230        return prices
1231
1232    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1233        """
1234        This method get and show information about all available broker instruments for current user account.
1235        If `instrumentsFile` string is not empty then also save information to this file.
1236
1237        :param show: if `True` then print results to console, if `False` - print only to file.
1238        :return: multi-lines string with all available broker instruments
1239        """
1240        if not self.iList:
1241            self.iList = self.Listing()
1242
1243        info = [
1244            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1245            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1246        ]
1247
1248        # add instruments count by type:
1249        for iType in self.iList.keys():
1250            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1251
1252        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1253        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1254
1255        # generating info tables with all instruments by type:
1256        for iType in self.iList.keys():
1257            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1258
1259            for instrument in self.iList[iType].keys():
1260                iName = self.iList[iType][instrument]["name"]  # instrument's name
1261                if len(iName) > 57:
1262                    iName = "{}...".format(iName[:54])  # right trim for a long string
1263
1264                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1265                    self.iList[iType][instrument]["ticker"],
1266                    iName,
1267                    self.iList[iType][instrument]["figi"],
1268                    self.iList[iType][instrument]["currency"],
1269                    self.iList[iType][instrument]["lot"],
1270                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1271                ))
1272
1273        infoText = "".join(info)
1274
1275        if show:
1276            uLogger.info(infoText)
1277
1278        if self.instrumentsFile:
1279            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1280                fH.write(infoText)
1281
1282            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1283
1284        return infoText
1285
1286    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1287        """
1288        This method search and show information about instruments by part of its ticker, FIGI or name.
1289        If `searchResultsFile` string is not empty then also save information to this file.
1290
1291        :param pattern: string with part of ticker, FIGI or instrument's name.
1292        :param show: if `True` then print results to console, if `False` - return list of result only.
1293        :return: list of dictionaries with all found instruments.
1294        """
1295        if not self.iList:
1296            self.iList = self.Listing()
1297
1298        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1299        compiledPattern = re.compile(pattern, re.IGNORECASE)
1300
1301        for iType in self.iList:
1302            for instrument in self.iList[iType].values():
1303                searchResult = compiledPattern.search(" ".join(
1304                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1305                ))
1306
1307                if searchResult:
1308                    searchResults[iType][instrument["ticker"]] = instrument
1309
1310        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1311        info = [
1312            "# Search results\n\n",
1313            "* **Search pattern:** [{}]\n".format(pattern),
1314            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1315            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1316        ]
1317        infoShort = info[:]
1318
1319        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1320        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1321        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1322
1323        if resultsLen == 0:
1324            info.append("\nNo results\n")
1325            infoShort.append("\nNo results\n")
1326            uLogger.warning("No results. Try changing your search pattern.")
1327
1328        else:
1329            for iType in searchResults:
1330                iTypeValuesCount = len(searchResults[iType].values())
1331                if iTypeValuesCount > 0:
1332                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1333                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1334
1335                    for instrument in searchResults[iType].values():
1336                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1337                            instrument["type"],
1338                            instrument["ticker"],
1339                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1340                            instrument["figi"],
1341                        ))
1342
1343                    if iTypeValuesCount <= 5:
1344                        infoShort.extend(info[-iTypeValuesCount:])
1345
1346                    else:
1347                        infoShort.extend(info[-5:])
1348                        infoShort.append(skippedLine)
1349
1350        infoText = "".join(info)
1351        infoTextShort = "".join(infoShort)
1352
1353        if show:
1354            uLogger.info(infoTextShort)
1355            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1356
1357        if self.searchResultsFile:
1358            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1359                fH.write(infoText)
1360
1361            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1362
1363        return searchResults
1364
1365    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1366        """
1367        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1368
1369        :param instruments: list of strings with tickers or FIGIs.
1370        :return: list with unique instrument FIGIs only.
1371        """
1372        requestedInstruments = []
1373        for iName in instruments:
1374            if iName not in self.aliases.keys():
1375                if iName not in requestedInstruments:
1376                    requestedInstruments.append(iName)
1377
1378            else:
1379                if iName not in requestedInstruments:
1380                    if self.aliases[iName] not in requestedInstruments:
1381                        requestedInstruments.append(self.aliases[iName])
1382
1383        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1384
1385        onlyUniqueFIGIs = []
1386        for iName in requestedInstruments:
1387            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1388                continue
1389
1390            self.ticker = iName
1391            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1392
1393            if not iData:
1394                self.ticker = ""
1395                self.figi = iName
1396
1397                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1398
1399                if not iData:
1400                    self.figi = ""
1401                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1402
1403            if iData and iData["figi"] not in onlyUniqueFIGIs:
1404                onlyUniqueFIGIs.append(iData["figi"])
1405
1406        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1407
1408        return onlyUniqueFIGIs
1409
1410    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1411        """
1412        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1413        See limits: https://tinkoff.github.io/investAPI/limits/
1414        If `pricesFile` string is not empty then also save information to this file.
1415
1416        :param instruments: list of strings with tickers or FIGIs.
1417        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1418        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1419                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1420        """
1421        if instruments is None or not instruments:
1422            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1423            raise Exception("Ticker or FIGI required")
1424
1425        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1426
1427        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1428
1429        iList = []  # trying to get info and current prices about all unique instruments:
1430        for self.figi in onlyUniqueFIGIs:
1431            iData = self.SearchByFIGI(requestPrice=True)
1432            iList.append(iData)
1433
1434        self.ShowListOfPrices(iList, show)
1435
1436        return iList
1437
1438    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1439        """
1440        Show table contains current prices of given instruments.
1441
1442        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1443                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1444        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1445        :return: multilines text in Markdown format as a table contains current prices.
1446        """
1447        infoText = ""
1448
1449        if show or self.pricesFile:
1450            info = [
1451                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1452                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1453                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1454            ]
1455
1456            for item in iList:
1457                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1458                    item["ticker"],
1459                    item["figi"],
1460                    item["type"],
1461                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1462                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1463                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1464                    "{} / {}".format(
1465                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1466                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1467                    ),
1468                    "{} / {}".format(
1469                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1470                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1471                    ),
1472                    item["currency"],
1473                ))
1474
1475            infoText = "".join(info)
1476
1477            if show:
1478                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1479
1480            if self.pricesFile:
1481                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1482                    fH.write(infoText)
1483
1484                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1485
1486        return infoText
1487
1488    def RequestTradingStatus(self) -> dict:
1489        """
1490        Requesting trading status for the instrument defined by `figi` variable.
1491        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1492        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1493
1494        :return: dictionary with trading status attributes. Response example:
1495                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1496                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1497        """
1498        if self.figi is None or not self.figi:
1499            uLogger.error("Variable `figi` must be defined for using this method!")
1500            raise Exception("FIGI required")
1501
1502        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1503
1504        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1505        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1506        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1507
1508        if self.moreDebug:
1509            uLogger.debug("Records about current trading status successfully received")
1510
1511        return tradingStatus
1512
1513    def RequestPortfolio(self) -> dict:
1514        """
1515        Requesting actual user's portfolio for current `accountId`.
1516        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1517        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1518
1519        :return: dictionary with user's portfolio.
1520        """
1521        if self.accountId is None or not self.accountId:
1522            uLogger.error("Variable `accountId` must be defined for using this method!")
1523            raise Exception("Account ID required")
1524
1525        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1526
1527        self.body = str({"accountId": self.accountId})
1528        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1529        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1530
1531        if self.moreDebug:
1532            uLogger.debug("Records about user's portfolio successfully received")
1533
1534        return rawPortfolio
1535
1536    def RequestPositions(self) -> dict:
1537        """
1538        Requesting open positions by currencies and instruments for current `accountId`.
1539        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1540        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1541
1542        :return: dictionary with open positions by instruments.
1543        """
1544        if self.accountId is None or not self.accountId:
1545            uLogger.error("Variable `accountId` must be defined for using this method!")
1546            raise Exception("Account ID required")
1547
1548        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1549
1550        self.body = str({"accountId": self.accountId})
1551        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1552        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1553
1554        if self.moreDebug:
1555            uLogger.debug("Records about current open positions successfully received")
1556
1557        return rawPositions
1558
1559    def RequestPendingOrders(self) -> list:
1560        """
1561        Requesting current actual pending orders for current `accountId`.
1562        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1563        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1564
1565        :return: list of dictionaries with pending orders.
1566        """
1567        if self.accountId is None or not self.accountId:
1568            uLogger.error("Variable `accountId` must be defined for using this method!")
1569            raise Exception("Account ID required")
1570
1571        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1572
1573        self.body = str({"accountId": self.accountId})
1574        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1575        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1576
1577        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1578
1579        return rawOrders
1580
1581    def RequestStopOrders(self) -> list:
1582        """
1583        Requesting current actual stop orders for current `accountId`.
1584        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1585        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1586
1587        :return: list of dictionaries with stop orders.
1588        """
1589        if self.accountId is None or not self.accountId:
1590            uLogger.error("Variable `accountId` must be defined for using this method!")
1591            raise Exception("Account ID required")
1592
1593        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1594
1595        self.body = str({"accountId": self.accountId})
1596        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1597        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1598
1599        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1600
1601        return rawStopOrders
1602
1603    def Overview(self, show: bool = False, details: str = "full") -> dict:
1604        """
1605        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1606        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1607        are defined then also save information to file.
1608
1609        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1610        many requests about the state of the portfolio, and then, based on the received data, a large number
1611        of calculation and statistics are collected.
1612
1613        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1614        :param details: how detailed should the information be? You should specify one of strings:
1615                        `full` - shows full available information about portfolio status (by default),
1616                        `positions` - shows only open positions,
1617                        `digest` - show a short digest of the portfolio status,
1618                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1619                        `orders` - shows only sections of open limits and stop orders.
1620        :return: dictionary with client's raw portfolio and some statistics.
1621        """
1622        if self.accountId is None or not self.accountId:
1623            uLogger.error("Variable `accountId` must be defined for using this method!")
1624            raise Exception("Account ID required")
1625
1626        view = {
1627            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1628                "headers": {},  # list of dictionaries, response headers without "positions" section
1629                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1630                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1631                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1632                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1633                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1634                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1635                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1636                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1637                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1638            },
1639            "stat": {  # --- some statistics calculated using "raw" sections:
1640                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1641                "availableRUB": 0.,  # available rubles (without other currencies)
1642                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1643                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1644                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1645                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1646                "sharesCostRUB": 0.,  # costs of all shares in RUB
1647                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1648                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1649                "futuresCostRUB": 0.,  # costs of all futures in RUB
1650                "Currencies": [],  # list of dictionaries of all currencies statistics
1651                "Shares": [],  # list of dictionaries of all shares statistics
1652                "Bonds": [],  # list of dictionaries of all bonds statistics
1653                "Etfs": [],  # list of dictionaries of all etfs statistics
1654                "Futures": [],  # list of dictionaries of all futures statistics
1655                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1656                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1657                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1658                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1659                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1660            },
1661            "analytics": {  # --- some analytics of portfolio:
1662                "distrByAssets": {},  # portfolio distribution by assets
1663                "distrByCompanies": {},  # portfolio distribution by companies
1664                "distrBySectors": {},  # portfolio distribution by sectors
1665                "distrByCurrencies": {},  # portfolio distribution by currencies
1666                "distrByCountries": {},  # portfolio distribution by countries
1667            }
1668        }
1669
1670        details = details.lower()
1671        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1672        if details not in availableDetails:
1673            details = "full"
1674            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1675
1676        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1677
1678        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1679        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1680        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1681        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1682
1683        # save response headers without "positions" section:
1684        for key in portfolioResponse.keys():
1685            if key != "positions":
1686                view["raw"]["headers"][key] = portfolioResponse[key]
1687
1688            else:
1689                continue
1690
1691        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1692        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1693        for item in portfolioResponse["positions"]:
1694            if item["instrumentType"] == "currency":
1695                self.figi = item["figi"]
1696                curr = self.SearchByFIGI(requestPrice=False)
1697
1698                # current price of currency in RUB:
1699                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1700                    "name": curr["name"],
1701                    "currentPrice": NanoToFloat(
1702                        item["currentPrice"]["units"],
1703                        item["currentPrice"]["nano"]
1704                    ),
1705                }
1706
1707                view["raw"]["Currencies"].append(item)
1708
1709            elif item["instrumentType"] == "share":
1710                view["raw"]["Shares"].append(item)
1711
1712            elif item["instrumentType"] == "bond":
1713                view["raw"]["Bonds"].append(item)
1714
1715            elif item["instrumentType"] == "etf":
1716                view["raw"]["Etfs"].append(item)
1717
1718            elif item["instrumentType"] == "futures":
1719                view["raw"]["Futures"].append(item)
1720
1721            else:
1722                continue
1723
1724        # how many volume of currencies (by ISO currency name) are blocked:
1725        for item in view["raw"]["positions"]["blocked"]:
1726            blocked = NanoToFloat(item["units"], item["nano"])
1727            if blocked > 0:
1728                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1729
1730        # how many volume of instruments (by FIGI) are blocked:
1731        for item in view["raw"]["positions"]["securities"]:
1732            blocked = int(item["blocked"])
1733            if blocked > 0:
1734                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1735
1736        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1737
1738        if "rub" in allBlocked.keys():
1739            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1740
1741        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1742        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1743        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1744        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1745        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1746        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1747        view["stat"]["portfolioCostRUB"] = sum([
1748            view["stat"]["allCurrenciesCostRUB"],
1749            view["stat"]["sharesCostRUB"],
1750            view["stat"]["bondsCostRUB"],
1751            view["stat"]["etfsCostRUB"],
1752            view["stat"]["futuresCostRUB"],
1753        ])
1754
1755        # --- calculating some portfolio statistics:
1756        byComp = {}  # distribution by companies
1757        bySect = {}  # distribution by sectors
1758        byCurr = {}  # distribution by currencies (include RUB)
1759        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1760        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1761
1762        for item in portfolioResponse["positions"]:
1763            self.figi = item["figi"]
1764            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1765
1766            if instrument:
1767                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1768                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1769
1770                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1771                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1772
1773                else:
1774                    blocked = 0
1775
1776                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1777                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1778                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1779                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1780                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1781                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1782                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1783                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1784                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1785                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1786                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1787                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1788
1789                statData = {
1790                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1791                    "ticker": instrument["ticker"],  # ticker by FIGI
1792                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1793                    "volume": volume,  # available volume of instrument
1794                    "lots": lots,  # volume in lots of instrument
1795                    "direction": direction,  # direction of an instrument's position: short or long
1796                    "blocked": blocked,  # blocked volume of currency or instrument
1797                    "currentPrice": curPrice,  # current instrument's price in basic asset
1798                    "average": average,  # current average position price
1799                    "cost": cost,  # current cost of all volume of instrument in basic asset
1800                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1801                    "costRUB": costRUB,  # cost of instrument in ruble
1802                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1803                    "profit": profit,  # expected profit at current moment
1804                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1805                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1806                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1807                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1808                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1809                    "step": instrument["step"],  # minimum price increment
1810                }
1811
1812                # adding distribution by unique countries:
1813                if statData["country"] not in byCountry.keys():
1814                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1815
1816                else:
1817                    byCountry[statData["country"]]["cost"] += costRUB
1818                    byCountry[statData["country"]]["percent"] += percentCostRUB
1819
1820                if item["instrumentType"] != "currency":
1821                    # adding distribution by unique companies:
1822                    if statData["name"]:
1823                        if statData["name"] not in byComp.keys():
1824                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1825
1826                        else:
1827                            byComp[statData["name"]]["cost"] += costRUB
1828                            byComp[statData["name"]]["percent"] += percentCostRUB
1829
1830                    # adding distribution by unique sectors:
1831                    if statData["sector"] not in bySect.keys():
1832                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1833
1834                    else:
1835                        bySect[statData["sector"]]["cost"] += costRUB
1836                        bySect[statData["sector"]]["percent"] += percentCostRUB
1837
1838                # adding distribution by unique currencies:
1839                if currency not in byCurr.keys():
1840                    byCurr[currency] = {
1841                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1842                        "cost": costRUB,
1843                        "percent": percentCostRUB
1844                    }
1845
1846                else:
1847                    byCurr[currency]["cost"] += costRUB
1848                    byCurr[currency]["percent"] += percentCostRUB
1849
1850                # saving statistics for every instrument:
1851                if item["instrumentType"] == "currency":
1852                    view["stat"]["Currencies"].append(statData)
1853
1854                    # update dict with free funds for trading (total - blocked) by currencies
1855                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1856                    view["stat"]["funds"][currency] = {
1857                        "total": volume,
1858                        "totalCostRUB": costRUB,  # total volume cost in rubles
1859                        "free": volume - blocked,
1860                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1861                    }
1862
1863                elif item["instrumentType"] == "share":
1864                    view["stat"]["Shares"].append(statData)
1865
1866                elif item["instrumentType"] == "bond":
1867                    view["stat"]["Bonds"].append(statData)
1868
1869                elif item["instrumentType"] == "etf":
1870                    view["stat"]["Etfs"].append(statData)
1871
1872                elif item["instrumentType"] == "Futures":
1873                    view["stat"]["Futures"].append(statData)
1874
1875                else:
1876                    continue
1877
1878        # total changes in Russian Ruble:
1879        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1880        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1881        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1882        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1883        view["stat"]["funds"]["rub"] = {
1884            "total": view["stat"]["availableRUB"],
1885            "totalCostRUB": view["stat"]["availableRUB"],
1886            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1887            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1888        }
1889
1890        # --- pending orders sector data:
1891        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1892        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1893
1894        for item in view["raw"]["orders"]:
1895            self.figi = item["figi"]
1896
1897            if item["figi"] not in uniquePendingOrdersFIGIs:
1898                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1899
1900                uniquePendingOrdersFIGIs.append(item["figi"])
1901                uniquePendingOrders[item["figi"]] = instrument
1902
1903            else:
1904                instrument = uniquePendingOrders[item["figi"]]
1905
1906            if instrument:
1907                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1908                orderType = TKS_ORDER_TYPES[item["orderType"]]
1909                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1910                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1911
1912                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1913                if item["direction"] == "ORDER_DIRECTION_BUY":
1914                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1915
1916                else:
1917                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1918
1919                # requested price for order execution:
1920                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1921
1922                # necessary changes in percent to reach target from current price:
1923                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1924
1925                view["stat"]["orders"].append({
1926                    "orderID": item["orderId"],  # orderId number parameter of current order
1927                    "figi": item["figi"],  # FIGI identification
1928                    "ticker": instrument["ticker"],  # ticker name by FIGI
1929                    "lotsRequested": item["lotsRequested"],  # requested lots value
1930                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1931                    "currentPrice": lastPrice,  # current instrument's price for defined action
1932                    "targetPrice": target,  # requested price for order execution in base currency
1933                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1934                    "percentChanges": changes,  # changes in percent to target from current price
1935                    "currency": item["currency"],  # instrument's currency name
1936                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1937                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1938                    "status": orderState,  # order status from TKS_ORDER_STATES
1939                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1940                })
1941
1942        # --- stop orders sector data:
1943        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1944        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1945
1946        for item in view["raw"]["stopOrders"]:
1947            self.figi = item["figi"]
1948
1949            if item["figi"] not in uniqueStopOrdersFIGIs:
1950                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1951
1952                uniqueStopOrdersFIGIs.append(item["figi"])
1953                uniqueStopOrders[item["figi"]] = instrument
1954
1955            else:
1956                instrument = uniqueStopOrders[item["figi"]]
1957
1958            if instrument:
1959                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1960                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1961                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1962
1963                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1964                if "expirationTime" in item.keys():
1965                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1966                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1967
1968                else:
1969                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1970                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1971
1972                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1973                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1974                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1975
1976                else:
1977                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1978
1979                # requested price when stop-order executed:
1980                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1981
1982                # price for limit-order, set up when stop-order executed:
1983                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1984
1985                # necessary changes in percent to reach target from current price:
1986                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1987
1988                view["stat"]["stopOrders"].append({
1989                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1990                    "figi": item["figi"],  # FIGI identification
1991                    "ticker": instrument["ticker"],  # ticker name by FIGI
1992                    "lotsRequested": item["lotsRequested"],  # requested lots value
1993                    "currentPrice": lastPrice,  # current instrument's price for defined action
1994                    "targetPrice": target,  # requested price for stop-order execution in base currency
1995                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1996                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1997                    "percentChanges": changes,  # changes in percent to target from current price
1998                    "currency": item["currency"],  # instrument's currency name
1999                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2000                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2001                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2002                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2003                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2004                })
2005
2006        # --- calculating data for analytics section:
2007        # portfolio distribution by assets:
2008        view["analytics"]["distrByAssets"] = {
2009            "Ruble": {
2010                "uniques": 1,
2011                "cost": view["stat"]["availableRUB"],
2012                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2013            },
2014            "Currencies": {
2015                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2016                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2017                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2018            },
2019            "Shares": {
2020                "uniques": len(view["stat"]["Shares"]),
2021                "cost": view["stat"]["sharesCostRUB"],
2022                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2023            },
2024            "Bonds": {
2025                "uniques": len(view["stat"]["Bonds"]),
2026                "cost": view["stat"]["bondsCostRUB"],
2027                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2028            },
2029            "Etfs": {
2030                "uniques": len(view["stat"]["Etfs"]),
2031                "cost": view["stat"]["etfsCostRUB"],
2032                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2033            },
2034            "Futures": {
2035                "uniques": len(view["stat"]["Futures"]),
2036                "cost": view["stat"]["futuresCostRUB"],
2037                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038            },
2039        }
2040
2041        # portfolio distribution by companies:
2042        view["analytics"]["distrByCompanies"]["All money cash"] = {
2043            "ticker": "",
2044            "cost": view["stat"]["allCurrenciesCostRUB"],
2045            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2046        }
2047        view["analytics"]["distrByCompanies"].update(byComp)
2048
2049        # portfolio distribution by sectors:
2050        view["analytics"]["distrBySectors"]["All money cash"] = {
2051            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2052            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2053        }
2054        view["analytics"]["distrBySectors"].update(bySect)
2055
2056        # portfolio distribution by currencies:
2057        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2058            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2059
2060            if self.moreDebug:
2061                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2062
2063        view["analytics"]["distrByCurrencies"].update(byCurr)
2064        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2065        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2066
2067        # portfolio distribution by countries:
2068        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2069            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2070
2071            if self.moreDebug:
2072                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2073
2074        view["analytics"]["distrByCountries"].update(byCountry)
2075        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2076        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2077
2078        # --- Prepare text statistics overview in human-readable:
2079        if show:
2080            # Whatever the value `details`, header not changes:
2081            info = [
2082                "# Client's portfolio\n\n",
2083                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2084                "* **Account ID:** [{}]\n".format(self.accountId),
2085            ]
2086
2087            if details in ["full", "positions", "digest"]:
2088                info.extend([
2089                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2090                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2091                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2092                        view["stat"]["totalChangesRUB"],
2093                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2094                        view["stat"]["totalChangesPercentRUB"],
2095                    ),
2096                ])
2097
2098            if details in ["full", "positions"]:
2099                info.extend([
2100                    "## Open positions\n\n",
2101                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2102                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2103                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2104                        "{:.2f} ({:.2f}) rub".format(
2105                            view["stat"]["availableRUB"],
2106                            view["stat"]["blockedRUB"],
2107                        )
2108                    )
2109                ])
2110
2111                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2112                    return [
2113                        "|                             |                                 |          |              |              |                     |                              |\n",
2114                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2115                            noTradeStr if noTradeStr else typeStr,
2116                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2117                        ),
2118                    ]
2119
2120                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2121                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2122                        "{} [{}]".format(data["ticker"], data["figi"]),
2123                        "{:.2f} ({:.2f}) {}".format(
2124                            data["volume"],
2125                            data["blocked"],
2126                            data["currency"],
2127                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2128                            data["volume"],
2129                            data["blocked"],
2130                        ),
2131                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2132                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2133                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2134                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2135                        "{}{:.2f} {} ({}{:.2f}%)".format(
2136                            "+" if data["profit"] > 0 else "",
2137                            data["profit"], data["baseCurrencyName"],
2138                            "+" if data["percentProfit"] > 0 else "",
2139                            data["percentProfit"],
2140                        ),
2141                    )
2142
2143                # --- Show currencies section:
2144                if view["stat"]["Currencies"]:
2145                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2146                    for item in view["stat"]["Currencies"]:
2147                        info.append(_InfoStr(item, showCurrencyName=True))
2148
2149                else:
2150                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2151
2152                # --- Show shares section:
2153                if view["stat"]["Shares"]:
2154                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2155
2156                    for item in view["stat"]["Shares"]:
2157                        info.append(_InfoStr(item))
2158
2159                else:
2160                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2161
2162                # --- Show bonds section:
2163                if view["stat"]["Bonds"]:
2164                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2165
2166                    for item in view["stat"]["Bonds"]:
2167                        info.append(_InfoStr(item))
2168
2169                else:
2170                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2171
2172                # --- Show etfs section:
2173                if view["stat"]["Etfs"]:
2174                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2175
2176                    for item in view["stat"]["Etfs"]:
2177                        info.append(_InfoStr(item))
2178
2179                else:
2180                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2181
2182                # --- Show futures section:
2183                if view["stat"]["Futures"]:
2184                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2185
2186                    for item in view["stat"]["Futures"]:
2187                        info.append(_InfoStr(item))
2188
2189                else:
2190                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2191
2192            if details in ["full", "orders"]:
2193                # --- Show pending orders section:
2194                if view["stat"]["orders"]:
2195                    info.extend([
2196                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2197                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2198                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2199                    ])
2200
2201                    for item in view["stat"]["orders"]:
2202                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2203                            "{} [{}]".format(item["ticker"], item["figi"]),
2204                            item["orderID"],
2205                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2206                            "{} {} ({}{:.2f}%)".format(
2207                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2208                                item["baseCurrencyName"],
2209                                "+" if item["percentChanges"] > 0 else "",
2210                                float(item["percentChanges"]),
2211                            ),
2212                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2213                            item["action"],
2214                            item["type"],
2215                            item["date"],
2216                        ))
2217
2218                else:
2219                    info.append("\n## Total pending limit-orders: 0\n")
2220
2221                # --- Show stop orders section:
2222                if view["stat"]["stopOrders"]:
2223                    info.extend([
2224                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2225                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2226                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2227                    ])
2228
2229                    for item in view["stat"]["stopOrders"]:
2230                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2231                            "{} [{}]".format(item["ticker"], item["figi"]),
2232                            item["orderID"],
2233                            item["lotsRequested"],
2234                            "{} {} ({}{:.2f}%)".format(
2235                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2236                                item["baseCurrencyName"],
2237                                "+" if item["percentChanges"] > 0 else "",
2238                                float(item["percentChanges"]),
2239                            ),
2240                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2241                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2242                            item["action"],
2243                            item["type"],
2244                            item["expType"],
2245                            item["createDate"],
2246                            item["expDate"],
2247                        ))
2248
2249                else:
2250                    info.append("\n## Total stop-orders: 0\n")
2251
2252            if details in ["full", "analytics"]:
2253                # -- Show analytics section:
2254                if view["stat"]["portfolioCostRUB"] > 0:
2255                    info.extend([
2256                        "\n# Analytics\n"
2257                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2258                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2259                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2260                            view["stat"]["totalChangesRUB"],
2261                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2262                            view["stat"]["totalChangesPercentRUB"],
2263                        ),
2264                        "\n## Portfolio distribution by assets\n"
2265                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2266                        "|------------|---------|---------|--------------------|\n",
2267                    ])
2268
2269                    for key in view["analytics"]["distrByAssets"].keys():
2270                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2271                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2272                                key,
2273                                view["analytics"]["distrByAssets"][key]["uniques"],
2274                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2275                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2276                            ))
2277
2278                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2279                    info.extend([
2280                        "\n## Portfolio distribution by companies\n"
2281                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2282                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2283                    ])
2284
2285                    for company in view["analytics"]["distrByCompanies"].keys():
2286                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2287                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2288                            info.append("| {} | {:<7} | {:<18} |\n".format(
2289                                "{}{}{}".format(
2290                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2291                                    company,
2292                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2293                                ),
2294                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2295                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2296                            ))
2297
2298                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2299                    info.extend([
2300                        "\n## Portfolio distribution by sectors\n"
2301                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2302                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2303                    ])
2304
2305                    for sector in view["analytics"]["distrBySectors"].keys():
2306                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2307                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2308                                sector,
2309                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2310                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2311                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2312                            ))
2313
2314                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2315                    info.extend([
2316                        "\n## Portfolio distribution by currencies\n"
2317                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2318                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2319                    ])
2320
2321                    for curr in view["analytics"]["distrByCurrencies"].keys():
2322                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2323                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2324                            info.append("| {} | {:<7} | {:<18} |\n".format(
2325                                "[{}] {}{}".format(
2326                                    curr,
2327                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2328                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2329                                ),
2330                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2331                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2332                            ))
2333
2334                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2335                    info.extend([
2336                        "\n## Portfolio distribution by countries\n"
2337                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2338                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2339                    ])
2340
2341                    for country in view["analytics"]["distrByCountries"].keys():
2342                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2343                            nameLen = len(country)
2344                            info.append("| {} | {:<7} | {:<18} |\n".format(
2345                                "{}{}".format(
2346                                    country,
2347                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2348                                ),
2349                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2350                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2351                            ))
2352
2353            infoText = "".join(info)
2354
2355            uLogger.info(infoText)
2356
2357            if details == "full" and self.overviewFile:
2358                filename = self.overviewFile
2359
2360            elif details == "digest" and self.overviewDigestFile:
2361                filename = self.overviewDigestFile
2362
2363            elif details == "positions" and self.overviewPositionsFile:
2364                filename = self.overviewPositionsFile
2365
2366            elif details == "orders" and self.overviewOrdersFile:
2367                filename = self.overviewOrdersFile
2368
2369            elif details == "analytics" and self.overviewAnalyticsFile:
2370                filename = self.overviewAnalyticsFile
2371
2372            else:
2373                filename = ""
2374
2375            if filename:
2376                with open(filename, "w", encoding="UTF-8") as fH:
2377                    fH.write(infoText)
2378
2379                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2380
2381        return view
2382
2383    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2384        """
2385        Returns history operations between two given dates for current `accountId`.
2386        If `reportFile` string is not empty then also save human-readable report.
2387        Shows some statistical data of closed positions.
2388
2389        :param start: see docstring in `GetDatesAsString()` method
2390        :param end: see docstring in `GetDatesAsString()` method
2391        :param show: if `True` then also prints all records to the console.
2392        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2393        :return: original list of dictionaries with history of deals records from API ("operations" key):
2394                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2395                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2396        """
2397        if self.accountId is None or not self.accountId:
2398            uLogger.error("Variable `accountId` must be defined for using this method!")
2399            raise Exception("Account ID required")
2400
2401        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2402
2403        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2404
2405        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2406        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2407        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2408        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2409        customStat = {}  # custom statistics in additional to responseJSON
2410
2411        # --- output report in human-readable format:
2412        if show or self.reportFile:
2413            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2414            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2415            nextDay = ""
2416
2417            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2418
2419            if len(ops) > 0:
2420                customStat = {
2421                    "opsCount": 0,  # total operations count
2422                    "buyCount": 0,  # buy operations
2423                    "sellCount": 0,  # sell operations
2424                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2425                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2426                    "payIn": {"rub": 0.},  # Deposit brokerage account
2427                    "payOut": {"rub": 0.},  # Withdrawals
2428                    "divs": {"rub": 0.},  # Dividends income
2429                    "coupons": {"rub": 0.},  # Coupon's income
2430                    "brokerCom": {"rub": 0.},  # Service commissions
2431                    "serviceCom": {"rub": 0.},  # Service commissions
2432                    "marginCom": {"rub": 0.},  # Margin commissions
2433                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2434                }
2435
2436                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2437                for item in ops:
2438                    if item["state"] == "OPERATION_STATE_EXECUTED":
2439                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2440
2441                        # count buy operations:
2442                        if "_BUY" in item["operationType"]:
2443                            customStat["buyCount"] += 1
2444
2445                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2446                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2447
2448                            else:
2449                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2450
2451                        # count sell operations:
2452                        elif "_SELL" in item["operationType"]:
2453                            customStat["sellCount"] += 1
2454
2455                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2456                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2457
2458                            else:
2459                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2460
2461                        # count incoming operations:
2462                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2463                            if item["payment"]["currency"] in customStat["payIn"].keys():
2464                                customStat["payIn"][item["payment"]["currency"]] += payment
2465
2466                            else:
2467                                customStat["payIn"][item["payment"]["currency"]] = payment
2468
2469                        # count withdrawals operations:
2470                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2471                            if item["payment"]["currency"] in customStat["payOut"].keys():
2472                                customStat["payOut"][item["payment"]["currency"]] += payment
2473
2474                            else:
2475                                customStat["payOut"][item["payment"]["currency"]] = payment
2476
2477                        # count dividends income:
2478                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2479                            if item["payment"]["currency"] in customStat["divs"].keys():
2480                                customStat["divs"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["divs"][item["payment"]["currency"]] = payment
2484
2485                        # count coupon's income:
2486                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2487                            if item["payment"]["currency"] in customStat["coupons"].keys():
2488                                customStat["coupons"][item["payment"]["currency"]] += payment
2489
2490                            else:
2491                                customStat["coupons"][item["payment"]["currency"]] = payment
2492
2493                        # count broker commissions:
2494                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2495                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2496                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2497
2498                            else:
2499                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2500
2501                        # count service commissions:
2502                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2503                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2504                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2505
2506                            else:
2507                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2508
2509                        # count margin commissions:
2510                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2511                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2512                                customStat["marginCom"][item["payment"]["currency"]] += payment
2513
2514                            else:
2515                                customStat["marginCom"][item["payment"]["currency"]] = payment
2516
2517                        # count withholding taxes:
2518                        elif "_TAX" in item["operationType"]:
2519                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2520                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2521
2522                            else:
2523                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2524
2525                        else:
2526                            continue
2527
2528                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2529
2530                # --- view "Actions" lines:
2531                info.extend([
2532                    "| Report sections            |                               |                              |                      |                        |\n",
2533                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2534                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2535                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2536                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2537                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2538                    ),
2539                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2540                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2541                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2542                    ),
2543                ])
2544
2545                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2546                for key in opsKeys:
2547                    if key == "rub":
2548                        continue
2549
2550                    info.extend([
2551                        "|                            |                               | {:<28} |                      |                        |\n".format(
2552                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2553                        ),
2554                        "|                            |                               | {:<28} |                      |                        |\n".format(
2555                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2556                        ),
2557                    ])
2558
2559                info.append(splitLine1)
2560
2561                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2562                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2563                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2564                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2565                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2566                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2567                    )
2568
2569                # --- view "Payments" lines:
2570                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2571                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2572
2573                for key in paymentsKeys:
2574                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2575
2576                info.append(splitLine1)
2577
2578                # --- view "Commissions and taxes" lines:
2579                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2580                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2581
2582                for key in comKeys:
2583                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2584
2585                info.append(splitLine1)
2586
2587                info.extend([
2588                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2589                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2590                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2591                ])
2592
2593            else:
2594                info.append("Broker returned no operations during this period\n")
2595
2596            # --- view "Operations" section:
2597            for item in ops:
2598                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2599                    continue
2600
2601                else:
2602                    self.figi = item["figi"] if item["figi"] else ""
2603                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2604                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2605
2606                    # group of deals during one day:
2607                    if nextDay and item["date"].split("T")[0] != nextDay:
2608                        info.append(splitLine2)
2609                        nextDay = ""
2610
2611                    else:
2612                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2613
2614                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2615                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2616                        self.figi if self.figi else "—",
2617                        instrument["ticker"] if instrument else "—",
2618                        instrument["type"] if instrument else "—",
2619                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2620                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2621                        TKS_OPERATION_STATES[item["state"]],
2622                        TKS_OPERATION_TYPES[item["operationType"]],
2623                    ))
2624
2625            infoText = "".join(info)
2626
2627            if show:
2628                if self.moreDebug:
2629                    uLogger.debug("Records about history of a client's operations successfully received")
2630
2631                uLogger.info(infoText)
2632
2633            if self.reportFile:
2634                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2635                    fH.write(infoText)
2636
2637                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2638
2639        return ops, customStat
2640
2641    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2642        """
2643        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2644
2645        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2646        Warning! Broker server used ISO UTC time by default.
2647
2648        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2649        Also, `historyFile` used to update history with `onlyMissing` parameter.
2650
2651        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2652
2653        :param start: see docstring in `GetDatesAsString()` method.
2654        :param end: see docstring in `GetDatesAsString()` method.
2655        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2656                         `"hour"`, `"day"`. Default: `"hour"`.
2657        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2658                            False by default. Warning! History appends only from last candle to current time
2659                            with always update last candle!
2660        :param csvSep: separator if csv-file is used, `,` by default.
2661        :param show: if `True` then also prints Pandas DataFrame to the console.
2662        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2663                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2664        """
2665        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2666        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2667        history = None  # empty pandas object for history
2668
2669        if interval not in TKS_CANDLE_INTERVALS.keys():
2670            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2671            raise Exception("Incorrect value")
2672
2673        if not (self.ticker or self.figi):
2674            uLogger.error("Ticker or FIGI must be defined!")
2675            raise Exception("Ticker or FIGI required")
2676
2677        if self.ticker and not self.figi:
2678            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2679            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2680
2681        if self.figi and not self.ticker:
2682            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2683            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2684
2685        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2686        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2687        if interval.lower() != "day":
2688            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2689
2690        delta = dtEnd - dtStart  # current UTC time minus last time in file
2691        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2692
2693        # calculate history length in candles:
2694        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2695        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2696            length += 1  # to avoid fraction time
2697
2698        # calculate data blocks count:
2699        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2700
2701        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2702        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2703        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2704        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2705        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2706
2707        tempOld = None  # pandas object for old history, if --only-missing key present
2708        lastTime = None  # datetime object of last old candle in file
2709
2710        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2711            uLogger.debug("--only-missing key present, add only last missing candles...")
2712            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2713
2714            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2715
2716            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2717            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2718            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2719            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2720
2721            # get last datetime object from last string in file or minus 1 delta if file is empty:
2722            if len(tempOld) > 0:
2723                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2724
2725            else:
2726                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2727
2728            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2729
2730        responseJSONs = []  # raw history blocks of data
2731
2732        blockEnd = dtEnd
2733        for item in range(blocks):
2734            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2735            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2736
2737            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2738                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2739            ))
2740
2741            if blockStart == blockEnd:
2742                uLogger.debug("Skipped this zero-length block...")
2743
2744            else:
2745                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2746                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2747                self.body = str({
2748                    "figi": self.figi,
2749                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2750                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2751                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2752                })
2753                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2754
2755                if "code" in responseJSON.keys():
2756                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2757
2758                else:
2759                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2760                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2761
2762                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2763
2764            blockEnd = blockStart
2765
2766        printCount = len(responseJSONs)  # candles to show in console
2767        if responseJSONs:
2768            tempHistory = pd.DataFrame(
2769                data={
2770                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2771                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2772                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2773                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2774                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2775                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2776                    "volume": [int(item["volume"]) for item in responseJSONs],
2777                },
2778                index=range(len(responseJSONs)),
2779                columns=["date", "time", "open", "high", "low", "close", "volume"],
2780            )
2781            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2782            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2783
2784            # append only newest candles to old history if --only-missing key present:
2785            if onlyMissing and tempOld is not None and lastTime is not None:
2786                index = 0  # find start index in tempHistory data:
2787
2788                for i, item in tempHistory.iterrows():
2789                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2790
2791                    if curTime == lastTime:
2792                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2793                        index = i
2794                        printCount = index + 1
2795                        break
2796
2797                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2798
2799            else:
2800                history = tempHistory  # if no `--only-missing` key then load full data from server
2801
2802            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2803
2804        if history is not None and not history.empty:
2805            if show:
2806                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2807                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2808                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2809                ))
2810
2811        else:
2812            uLogger.warning("Received an empty candles history!")
2813
2814        if self.historyFile is not None:
2815            if history is not None and not history.empty:
2816                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2817                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2818
2819            else:
2820                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2821
2822        else:
2823            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2824
2825        return history
2826
2827    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2828        """
2829        Load candles history from csv-file and return Pandas DataFrame object.
2830
2831        See also: `History()` and `ShowHistoryChart()` methods.
2832
2833        :param filePath: path to csv-file to open.
2834        """
2835        loadedHistory = None  # init candles data object
2836
2837        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2838
2839        if os.path.exists(filePath):
2840            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2841
2842            tfStr = self.priceModel.FormattedDelta(
2843                self.priceModel.timeframe,
2844                "{days} days {hours}h {minutes}m {seconds}s",
2845            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2846                self.priceModel.timeframe,
2847                "{hours}h {minutes}m {seconds}s",
2848            )
2849
2850            if loadedHistory is not None and not loadedHistory.empty:
2851                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2852                    len(loadedHistory),
2853                    tfStr,
2854                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2855                )
2856
2857            else:
2858                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2859
2860        else:
2861            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2862
2863        return loadedHistory
2864
2865    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2866        """
2867        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2868
2869        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2870        Default: `index.html` (both for interact and non-interact candlesticks chart).
2871
2872        See also: `History()` and `LoadHistory()` methods.
2873
2874        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2875        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2876                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2877                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2878                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2879        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2880                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2881        """
2882        if isinstance(candles, str):
2883            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2884            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2885
2886        elif isinstance(candles, pd.DataFrame):
2887            self.priceModel.prices = candles  # set candles chain from variable
2888            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2889
2890            if "datetime" not in candles.columns:
2891                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2892
2893        else:
2894            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2895            raise Exception("Incorrect value")
2896
2897        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2898
2899        if interact:
2900            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2901
2902            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2903
2904        else:
2905            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2906
2907            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2908
2909        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2910
2911    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2912        """
2913        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2914        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2915
2916        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2917
2918        :param operation: string "Buy" or "Sell".
2919        :param lots: volume, integer count of lots >= 1.
2920        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2921        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2922        :param expDate: string "Undefined" by default or local date in future,
2923                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2924        :return: JSON with response from broker server.
2925        """
2926        if self.accountId is None or not self.accountId:
2927            uLogger.error("Variable `accountId` must be defined for using this method!")
2928            raise Exception("Account ID required")
2929
2930        if operation is None or not operation or operation not in ("Buy", "Sell"):
2931            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2932            raise Exception("Incorrect value")
2933
2934        if lots is None or lots < 1:
2935            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2936            lots = 1
2937
2938        if tp is None or tp < 0:
2939            tp = 0
2940
2941        if sl is None or sl < 0:
2942            sl = 0
2943
2944        if expDate is None or not expDate:
2945            expDate = "Undefined"
2946
2947        if not (self.ticker or self.figi):
2948            uLogger.error("Ticker or FIGI must be defined!")
2949            raise Exception("Ticker or FIGI required")
2950
2951        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2952        self.ticker = instrument["ticker"]
2953        self.figi = instrument["figi"]
2954
2955        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2956
2957        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2958        self.body = str({
2959            "figi": self.figi,
2960            "quantity": str(lots),
2961            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2962            "accountId": str(self.accountId),
2963            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2964        })
2965        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2966
2967        if "orderId" in response.keys():
2968            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2969                operation, response["orderId"],
2970                self.ticker, self.figi, lots,
2971                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2972                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2973                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2974            ))
2975
2976            if tp > 0:
2977                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2978
2979            if sl > 0:
2980                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2981
2982        else:
2983            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2984
2985        return response
2986
2987    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2988        """
2989        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2990        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2991
2992        See also: `Order()` and `Trade()` docstrings.
2993
2994        :param lots: volume, integer count of lots >= 1.
2995        :param tp: float > 0, take profit price of stop-order.
2996        :param sl: float > 0, stop loss price of stop-order.
2997        :param expDate: it's a local date in future.
2998                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2999        :return: JSON with response from broker server.
3000        """
3001        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3002
3003    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3004        """
3005        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3006        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3007
3008        See also: `Order()` and `Trade()` docstrings.
3009
3010        :param lots: volume, integer count of lots >= 1.
3011        :param tp: float > 0, take profit price of stop-order.
3012        :param sl: float > 0, stop loss price of stop-order.
3013        :param expDate: it's a local date in the future.
3014                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3015        :return: JSON with response from broker server.
3016        """
3017        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3018
3019    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3020        """
3021        Close position of given instruments.
3022
3023        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3024        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3025                         This avoids unnecessary downloading data from the server.
3026        """
3027        if instruments is None or not instruments:
3028            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3029            raise Exception("Ticker or FIGI required")
3030
3031        if isinstance(instruments, str):
3032            instruments = [instruments]
3033
3034        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3035        if uniqueInstruments:
3036            if portfolio is None or not portfolio:
3037                portfolio = self.Overview(show=False)
3038
3039            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3040            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3041
3042            for self.figi in uniqueInstruments:
3043                if self.figi not in allOpened:
3044                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3045                    continue
3046
3047                # search open trade info about instrument by ticker:
3048                instrument = {}
3049                for iType in TKS_INSTRUMENTS:
3050                    if instrument:
3051                        break
3052
3053                    for item in portfolio["stat"][iType]:
3054                        if item["figi"] == self.figi:
3055                            instrument = item
3056                            break
3057
3058                if instrument:
3059                    self.ticker = instrument["ticker"]
3060                    self.figi = instrument["figi"]
3061
3062                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3063                        self.ticker,
3064                        self.figi,
3065                        int(instrument["volume"]),
3066                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3067                    ))
3068
3069                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3070
3071                    if tradeLots > 0:
3072                        if instrument["blocked"] > 0:
3073                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3074                                instrument["blocked"],
3075                                self.ticker,
3076                                tradeLots,
3077                            ))
3078
3079                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3080                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3081
3082                    else:
3083                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3084
3085    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3086        """
3087        Close all positions of given instruments with defined type.
3088
3089        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3090        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3091                         This avoids unnecessary downloading data from the server.
3092        """
3093        if iType not in TKS_INSTRUMENTS:
3094            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3095
3096        else:
3097            if portfolio is None or not portfolio:
3098                portfolio = self.Overview(show=False)
3099
3100            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3101            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3102
3103            if tickers and portfolio:
3104                self.CloseTrades(tickers, portfolio)
3105
3106            else:
3107                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3108
3109    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3110        """
3111        Universal method to create market or limit orders with all available parameters for current `accountId`.
3112        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3113
3114        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3115        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3116
3117        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3118        then broker immediately open market order as you can do simple --buy or --sell operations!
3119
3120        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3121        When current price will go up or down to target price value then broker opens a limit order.
3122        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3123
3124        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3125
3126        :param operation: string "Buy" or "Sell".
3127        :param orderType: string "Limit" or "Stop".
3128        :param lots: volume, integer count of lots >= 1.
3129        :param targetPrice: target price > 0. This is open trade price for limit order.
3130        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3131                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3132        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3133                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3134                         Stop loss order always executed by market price.
3135        :param expDate: string "Undefined" by default or local date in future.
3136                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3137                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3138                        A limit order has no expiration date, it lasts until the end of the trading day.
3139        :return: JSON with response from broker server.
3140        """
3141        if self.accountId is None or not self.accountId:
3142            uLogger.error("Variable `accountId` must be defined for using this method!")
3143            raise Exception("Account ID required")
3144
3145        if operation is None or not operation or operation not in ("Buy", "Sell"):
3146            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3147            raise Exception("Incorrect value")
3148
3149        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3150            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3151            raise Exception("Incorrect value")
3152
3153        if lots is None or lots < 1:
3154            uLogger.error("You must define trade volume > 0: integer count of lots!")
3155            raise Exception("Incorrect value")
3156
3157        if targetPrice is None or targetPrice <= 0:
3158            uLogger.error("Target price for limit-order must be greater than 0!")
3159            raise Exception("Incorrect value")
3160
3161        if limitPrice is None or limitPrice <= 0:
3162            limitPrice = targetPrice
3163
3164        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3165            stopType = "Limit"
3166
3167        if expDate is None or not expDate:
3168            expDate = "Undefined"
3169
3170        if not (self.ticker or self.figi):
3171            uLogger.error("Tocker or FIGI must be defined!")
3172            raise Exception("Ticker or FIGI required")
3173
3174        response = {}
3175        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3176        self.ticker = instrument["ticker"]
3177        self.figi = instrument["figi"]
3178
3179        if orderType == "Limit":
3180            uLogger.debug(
3181                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3182                    self.ticker, self.figi,
3183                    operation, lots, targetPrice, instrument["currency"],
3184                ))
3185
3186            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3187            self.body = str({
3188                "figi": self.figi,
3189                "quantity": str(lots),
3190                "price": FloatToNano(targetPrice),
3191                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3192                "accountId": str(self.accountId),
3193                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3194            })
3195            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3196
3197            if "orderId" in response.keys():
3198                uLogger.info(
3199                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3200                        response["orderId"],
3201                        self.ticker, self.figi,
3202                        operation, lots, targetPrice, instrument["currency"],
3203                    ))
3204
3205                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3206                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3207                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3208                            targetPrice, instrument["currency"],
3209                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3210                        ))
3211
3212                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3213                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3214                            targetPrice, instrument["currency"],
3215                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3216                        ))
3217
3218            else:
3219                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3220
3221        if orderType == "Stop":
3222            uLogger.debug(
3223                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3224                    self.ticker, self.figi,
3225                    operation, lots,
3226                    targetPrice, instrument["currency"],
3227                    limitPrice, instrument["currency"],
3228                    stopType, expDate,
3229                ))
3230
3231            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3232            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3233            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3234
3235            body = {
3236                "figi": self.figi,
3237                "quantity": str(lots),
3238                "price": FloatToNano(limitPrice),
3239                "stopPrice": FloatToNano(targetPrice),
3240                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3241                "accountId": str(self.accountId),
3242                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3243                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3244            }
3245
3246            if expDateUTC:
3247                body["expireDate"] = expDateUTC
3248
3249            self.body = str(body)
3250            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3251
3252            if "stopOrderId" in response.keys():
3253                uLogger.info(
3254                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3255                        response["stopOrderId"],
3256                        self.ticker, self.figi,
3257                        operation, lots,
3258                        targetPrice, instrument["currency"],
3259                        limitPrice, instrument["currency"],
3260                        TKS_STOP_ORDER_TYPES[stopOrderType],
3261                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3262                    ))
3263
3264                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3265                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3266                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3267                            targetPrice, instrument["currency"],
3268                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3269                        ))
3270
3271                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3272                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3273                            targetPrice, instrument["currency"],
3274                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3275                        ))
3276
3277            else:
3278                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3279
3280        return response
3281
3282    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3283        """
3284        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3285        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3286        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3287        See also: `Order()` docstring.
3288
3289        :param lots: volume, integer count of lots >= 1.
3290        :param targetPrice: target price > 0. This is open trade price for limit order.
3291        :return: JSON with response from broker server.
3292        """
3293        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3294
3295    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3296        """
3297        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3298        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3299        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3300        target price value then broker opens a limit order. See also: `Order()` docstring.
3301
3302        :param lots: volume, integer count of lots >= 1.
3303        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3304        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3305                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3306        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3307                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3308        :param expDate: string "Undefined" by default or local date in future.
3309                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3310                        This date is converting to UTC format for server.
3311        :return: JSON with response from broker server.
3312        """
3313        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3314
3315    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3316        """
3317        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3318        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3319        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3320        See also: `Order()` docstring.
3321
3322        :param lots: volume, integer count of lots >= 1.
3323        :param targetPrice: target price > 0. This is open trade price for limit order.
3324        :return: JSON with response from broker server.
3325        """
3326        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3327
3328    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3329        """
3330        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3331        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3332        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3333        target price value then broker opens a limit order. See also: `Order()` docstring.
3334
3335        :param lots: volume, integer count of lots >= 1.
3336        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3337        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3338                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3339        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3340                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3341        :param expDate: string "Undefined" by default or local date in future.
3342                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3343                        This date is converting to UTC format for server.
3344        :return: JSON with response from broker server.
3345        """
3346        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3347
3348    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3349        """
3350        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3351
3352        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3353        :param allOrdersIDs: pre-received lists of all active pending orders.
3354                             This avoids unnecessary downloading data from the server.
3355        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3356        """
3357        if self.accountId is None or not self.accountId:
3358            uLogger.error("Variable `accountId` must be defined for using this method!")
3359            raise Exception("Account ID required")
3360
3361        if orderIDs:
3362            if allOrdersIDs is None or not allOrdersIDs:
3363                rawOrders = self.RequestPendingOrders()
3364                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3365
3366            if allStopOrdersIDs is None or not allStopOrdersIDs:
3367                rawStopOrders = self.RequestStopOrders()
3368                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3369
3370            for orderID in orderIDs:
3371                idInPendingOrders = orderID in allOrdersIDs
3372                idInStopOrders = orderID in allStopOrdersIDs
3373
3374                if not (idInPendingOrders or idInStopOrders):
3375                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3376                    continue
3377
3378                else:
3379                    if idInPendingOrders:
3380                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3381
3382                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3383                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3384                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3385                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3386
3387                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3388                            if self.moreDebug:
3389                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3390
3391                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3392
3393                        else:
3394                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3395
3396                    elif idInStopOrders:
3397                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3398
3399                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3400                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3401                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3402                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3403
3404                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3405                            if self.moreDebug:
3406                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3407
3408                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3409
3410                        else:
3411                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3412
3413                    else:
3414                        continue
3415
3416    def CloseAllOrders(self) -> None:
3417        """
3418        Gets a list of open pending and stop orders and cancel it all.
3419        """
3420        rawOrders = self.RequestPendingOrders()
3421        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3422        lenOrders = len(allOrdersIDs)
3423
3424        rawStopOrders = self.RequestStopOrders()
3425        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3426        lenSOrders = len(allStopOrdersIDs)
3427
3428        if lenOrders > 0 or lenSOrders > 0:
3429            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3430
3431            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3432
3433        else:
3434            uLogger.info("Orders not found, nothing to cancel.")
3435
3436    def CloseAll(self, *args) -> None:
3437        """
3438        Close all available (not blocked) opened trades and orders.
3439
3440        Also, you can select one or more keywords case-insensitive:
3441        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3442
3443        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3444        """
3445        overview = self.Overview(show=False)  # get all open trades info
3446
3447        if len(args) == 0:
3448            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3449            self.CloseAllOrders()  # close all pending and stop orders
3450
3451            for iType in TKS_INSTRUMENTS:
3452                if iType != "Currencies":
3453                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3454
3455        else:
3456            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3457            lowerArgs = [x.lower() for x in args]
3458
3459            if "orders" in lowerArgs:
3460                self.CloseAllOrders()  # close all pending and stop orders
3461
3462            for iType in TKS_INSTRUMENTS:
3463                if iType.lower() in lowerArgs and iType != "Currencies":
3464                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3465
3466    @staticmethod
3467    def ParseOrderParameters(operation, **inputParameters):
3468        """
3469        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3470
3471        :param operation: string "Buy" or "Sell".
3472        :param inputParameters: this is dict of strings that looks like this
3473               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3474               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3475               "prices" key: one or more prices to open limit-orders
3476               Counts of values in lots and prices lists must be equals!
3477        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3478        """
3479        # TODO: update order grid work with api v2
3480        pass
3481        # uLogger.debug("Input parameters: {}".format(inputParameters))
3482        #
3483        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3484        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3485        #     raise Exception("Incorrect value")
3486        #
3487        # if "l" in inputParameters.keys():
3488        #     inputParameters["lots"] = inputParameters.pop("l")
3489        #
3490        # if "p" in inputParameters.keys():
3491        #     inputParameters["prices"] = inputParameters.pop("p")
3492        #
3493        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3494        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3495        #     raise Exception("Incorrect value")
3496        #
3497        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3498        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3499        #
3500        # if len(lots) != len(prices):
3501        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3502        #     raise Exception("Incorrect value")
3503        #
3504        # uLogger.debug("Extracted parameters for orders:")
3505        # uLogger.debug("lots = {}".format(lots))
3506        # uLogger.debug("prices = {}".format(prices))
3507        #
3508        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3509        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3510        # uLogger.debug("Order parameters: {}".format(result))
3511        #
3512        # return result
3513
3514    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3515        """
3516        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3517
3518        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3519        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3520        """
3521        result = False
3522        msg = "Instrument not defined!"
3523
3524        if portfolio is None or not portfolio:
3525            portfolio = self.Overview(show=False)
3526
3527        if self.ticker:
3528            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3529            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3530
3531            for iType in TKS_INSTRUMENTS:
3532                for instrument in portfolio["stat"][iType]:
3533                    if instrument["ticker"] == self.ticker:
3534                        result = True
3535                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3536                        break
3537
3538        elif self.figi:
3539            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3540            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3541
3542            for iType in TKS_INSTRUMENTS:
3543                for instrument in portfolio["stat"][iType]:
3544                    if instrument["figi"] == self.figi:
3545                        result = True
3546                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3547                        break
3548
3549        else:
3550            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3551
3552        uLogger.debug(msg)
3553
3554        return result
3555
3556    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3557        """
3558        Returns instrument is in the user's portfolio if it presents there.
3559        Instrument must be defined by `ticker` (highly priority) or `figi`.
3560
3561        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3562        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3563        """
3564        result = None
3565        msg = "Instrument not defined!"
3566
3567        if portfolio is None or not portfolio:
3568            portfolio = self.Overview(show=False)
3569
3570        if self.ticker:
3571            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3572            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3573
3574            for iType in TKS_INSTRUMENTS:
3575                for instrument in portfolio["stat"][iType]:
3576                    if instrument["ticker"] == self.ticker:
3577                        result = instrument
3578                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3579                        break
3580
3581        elif self.figi:
3582            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3583            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3584
3585            for iType in TKS_INSTRUMENTS:
3586                for instrument in portfolio["stat"][iType]:
3587                    if instrument["figi"] == self.figi:
3588                        result = instrument
3589                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3590                        break
3591
3592        else:
3593            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3594
3595        uLogger.debug(msg)
3596
3597        return result
3598
3599    def RequestLimits(self) -> dict:
3600        """
3601        Method for obtaining the available funds for withdrawal for current `accountId`.
3602
3603        See also:
3604        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3605        - `OverviewLimits()` method
3606
3607        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3608                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3609                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3610                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3611        """
3612        if self.accountId is None or not self.accountId:
3613            uLogger.error("Variable `accountId` must be defined for using this method!")
3614            raise Exception("Account ID required")
3615
3616        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3617
3618        self.body = str({"accountId": self.accountId})
3619        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3620        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3621
3622        if self.moreDebug:
3623            uLogger.debug("Records about available funds for withdrawal successfully received")
3624
3625        return rawLimits
3626
3627    def OverviewLimits(self, show: bool = False) -> dict:
3628        """
3629        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3630
3631        See also: `RequestLimits()`.
3632
3633        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3634        :return: dict with raw parsed data from server and some calculated statistics about it.
3635        """
3636        if self.accountId is None or not self.accountId:
3637            uLogger.error("Variable `accountId` must be defined for using this method!")
3638            raise Exception("Account ID required")
3639
3640        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3641
3642        view = {
3643            "rawLimits": rawLimits,
3644            "limits": {  # parsed data for every currency:
3645                "money": {  # this is an array of portfolio currency positions
3646                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3647                },
3648                "blocked": {  # this is an array of blocked currency
3649                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3650                },
3651                "blockedGuarantee": {  # this is locked money under collateral for futures
3652                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3653                },
3654            },
3655        }
3656
3657        # --- Prepare text table with limits in human-readable format:
3658        if show:
3659            info = [
3660                "# Withdrawal limits\n\n",
3661                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3662                "* **Account ID:** [{}]\n".format(self.accountId),
3663            ]
3664
3665            if view["limits"]["money"]:
3666                info.extend([
3667                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3668                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3669                ])
3670
3671            else:
3672                info.append("\nNo withdrawal limits\n")
3673
3674            for curr in view["limits"]["money"].keys():
3675                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3676                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3677                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3678
3679                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3680                    "[{}]".format(curr),
3681                    "{:.2f}".format(view["limits"]["money"][curr]),
3682                    "{:.2f}".format(availableMoney),
3683                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3684                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3685                )
3686
3687                if curr == "rub":
3688                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3689
3690                else:
3691                    info.append(infoStr)
3692
3693            infoText = "".join(info)
3694
3695            uLogger.info(infoText)
3696
3697            if self.withdrawalLimitsFile:
3698                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3699                    fH.write(infoText)
3700
3701                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3702
3703        return view
3704
3705    def RequestAccounts(self) -> dict:
3706        """
3707        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3708
3709        See also:
3710        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3711        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3712        - `OverviewUserInfo()` method
3713
3714        :return: dict with raw data from server that contains accounts info. Example of dict:
3715                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3716                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3717                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3718                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3719        """
3720        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3721
3722        self.body = str({})
3723        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3724        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3725
3726        if self.moreDebug:
3727            uLogger.debug("Records about available accounts successfully received")
3728
3729        return rawAccounts
3730
3731    def RequestUserInfo(self) -> dict:
3732        """
3733        Method for requesting common user's information.
3734
3735        See also:
3736        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3737        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3738        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3739        - `OverviewUserInfo()` method
3740
3741        :return: dict with raw data from server that contains user's information. Example of dict:
3742                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3743                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3744        """
3745        uLogger.debug("Requesting common user's information. Wait, please...")
3746
3747        self.body = str({})
3748        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3749        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3750
3751        if self.moreDebug:
3752            uLogger.debug("Records about current user successfully received")
3753
3754        return rawUserInfo
3755
3756    def RequestMarginStatus(self, accountId: str = None) -> dict:
3757        """
3758        Method for requesting margin calculation for defined account ID.
3759
3760        See also:
3761        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3762        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3763        - `OverviewUserInfo()` method
3764
3765        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3766        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3767                 Example of responses:
3768                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3769                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3770                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3771                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3772                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3773                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3774        """
3775        if accountId is None or not accountId:
3776            if self.accountId is None or not self.accountId:
3777                uLogger.error("Variable `accountId` must be defined for using this method!")
3778                raise Exception("Account ID required")
3779
3780            else:
3781                accountId = self.accountId  # use `self.accountId` (main ID) by default
3782
3783        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3784
3785        self.body = str({"accountId": accountId})
3786        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3787        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3788
3789        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3790            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3791            rawMargin = {}
3792
3793        else:
3794            if self.moreDebug:
3795                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3796
3797        return rawMargin
3798
3799    def RequestTariffLimits(self) -> dict:
3800        """
3801        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3802
3803        See also:
3804        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3805        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3806        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3807        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3808        - `OverviewUserInfo()` method
3809
3810        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3811                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3812                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3813        """
3814        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3815
3816        self.body = str({})
3817        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3818        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3819
3820        if self.moreDebug:
3821            uLogger.debug("Records with limits of current tariff successfully received")
3822
3823        return rawTariffLimits
3824
3825    def RequestBondCoupons(self, iJSON: dict) -> dict:
3826        """
3827        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3828        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3829        All dates are in UTC timezone.
3830
3831        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3832        Documentation:
3833        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3834        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3835
3836        See also: `ExtendBondsData()`.
3837
3838        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3839                      If raw iJSON is not data of bond then server returns an error [400] with message:
3840                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3841        :return: dictionary with bond payment calendar. Response example
3842                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3843                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3844                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3845                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3846        """
3847        if iJSON["figi"] is None or not iJSON["figi"]:
3848            uLogger.error("FIGI must be defined for using this method!")
3849            raise Exception("FIGI required")
3850
3851        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3852        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3853
3854        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3855            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3856            self.figi,
3857            startDate,
3858            endDate,
3859        ))
3860
3861        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3862        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3863        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3864
3865        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3866            uLogger.warning("Instrument type is not bond!")
3867
3868        else:
3869            if self.moreDebug:
3870                uLogger.debug("Records about bond payment calendar successfully received")
3871
3872        return calendar
3873
3874    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3875        """
3876        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3877        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3878        coupon yields, current yields and some statistics etc.
3879
3880        WARNING! This is too long operation if a lot of bonds requested from broker server.
3881
3882        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3883
3884        :param instruments: list of strings with tickers or FIGIs.
3885        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3886                     for further used by data scientists or stock analytics.
3887        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3888                 In XLSX-file and Pandas DataFrame fields mean:
3889                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3890                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3891        """
3892        if instruments is None or not instruments:
3893            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3894            raise Exception("Ticker or FIGI required")
3895
3896        if isinstance(instruments, str):
3897            instruments = [instruments]
3898
3899        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3900
3901        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3902
3903        iCount = len(uniqueInstruments)
3904        tooLong = iCount >= 20
3905        if tooLong:
3906            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3907
3908        bonds = None
3909        for i, self.figi in enumerate(uniqueInstruments):
3910            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3911
3912            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3913                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3914                rawBond = self.SearchByFIGI(requestPrice=True)
3915
3916                # Widen raw data with UTC current time (iData["actualDateTime"]):
3917                actualDate = datetime.now(tzutc())
3918                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3919
3920                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3921                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3922
3923                # Replace some values with human-readable:
3924                iData["nominalCurrency"] = iData["nominal"]["currency"]
3925                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3926                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3927                iData["aciCurrency"] = iData["aciValue"]["currency"]
3928                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3929                iData["issueSize"] = int(iData["issueSize"])
3930                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3931                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3932                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3933                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3934                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3935                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3936                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3937                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3938                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3939                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3940
3941                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3942                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3943                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3944                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3945                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3946                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3947                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3948                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3949                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3950                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3951                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3952
3953                # Widen raw data with calendar data from `rawCalendar` values:
3954                calendarData = []
3955                if "events" in iData["rawCalendar"].keys():
3956                    for item in iData["rawCalendar"]["events"]:
3957                        calendarData.append({
3958                            "couponDate": item["couponDate"],
3959                            "couponNumber": int(item["couponNumber"]),
3960                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3961                            "payCurrency": item["payOneBond"]["currency"],
3962                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3963                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3964                            "couponStartDate": item["couponStartDate"],
3965                            "couponEndDate": item["couponEndDate"],
3966                            "couponPeriod": item["couponPeriod"],
3967                        })
3968
3969                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3970                    if "maturityDate" not in iData.keys():
3971                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3972
3973                # Widen raw data with Coupon Rate.
3974                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3975                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3976                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3977                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3978
3979                # Widen raw data with Yield to Maturity (YTM) on current date.
3980                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3981                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3982                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3983                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3984                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3985                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3986
3987                iData["calendar"] = calendarData  # adds calendar at the end
3988
3989                # Remove not used data:
3990                iData.pop("uid")
3991                iData.pop("positionUid")
3992                iData.pop("currentPrice")
3993                iData.pop("rawCalendar")
3994
3995                colNames = list(iData.keys())
3996                if bonds is None:
3997                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3998
3999                else:
4000                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4001
4002            else:
4003                uLogger.warning("Instrument is not a bond!")
4004
4005            processed = round(100 * (i + 1) / iCount, 1)
4006            if tooLong and processed % 5 == 0:
4007                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4008
4009            else:
4010                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4011
4012        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4013
4014        # Saving bonds from Pandas DataFrame to XLSX sheet:
4015        if xlsx and self.bondsXLSXFile:
4016            with pd.ExcelWriter(
4017                    path=self.bondsXLSXFile,
4018                    date_format=TKS_DATE_FORMAT,
4019                    datetime_format=TKS_DATE_TIME_FORMAT,
4020                    mode="w",
4021            ) as writer:
4022                bonds.to_excel(
4023                    writer,
4024                    sheet_name="Extended bonds data",
4025                    index=True,
4026                    encoding="UTF-8",
4027                    freeze_panes=(1, 1),
4028                )  # saving as XLSX-file with freeze first row and column as headers
4029
4030            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4031
4032        return bonds
4033
4034    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4035        """
4036        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4037
4038        WARNING! This is too long operation if a lot of bonds requested from broker server.
4039
4040        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4041
4042        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4043                        extended information about bonds: main info, current prices, bond payment calendar,
4044                        coupon yields, current yields and some statistics etc.
4045                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4046        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4047                     for further used by data scientists or stock analytics.
4048        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4049        """
4050        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4051            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4052
4053        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4054
4055        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4056        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4057        calendar = None
4058        for bond in extBonds.iterrows():
4059            for item in bond[1]["calendar"]:
4060                cData = {
4061                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4062                    "couponDate": item["couponDate"],
4063                    "figi": bond[1]["figi"],
4064                    "ticker": bond[1]["ticker"],
4065                    "name": bond[1]["name"],
4066                    "couponNumber": item["couponNumber"],
4067                    "payOneBond": item["payOneBond"],
4068                    "payCurrency": item["payCurrency"],
4069                    "couponType": item["couponType"],
4070                    "couponPeriod": item["couponPeriod"],
4071                    "fixDate": item["fixDate"],
4072                    "couponStartDate": item["couponStartDate"],
4073                    "couponEndDate": item["couponEndDate"],
4074                }
4075
4076                if calendar is None:
4077                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4078
4079                else:
4080                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4081
4082        if calendar is not None:
4083            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4084
4085            # Saving calendar from Pandas DataFrame to XLSX sheet:
4086            if xlsx:
4087                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4088
4089                with pd.ExcelWriter(
4090                        path=xlsxCalendarFile,
4091                        date_format=TKS_DATE_FORMAT,
4092                        datetime_format=TKS_DATE_TIME_FORMAT,
4093                        mode="w",
4094                ) as writer:
4095                    humanReadable = calendar.copy(deep=True)
4096                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4097                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4098                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4099                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4100                    humanReadable.columns = colNames  # human-readable column names
4101
4102                    humanReadable.to_excel(
4103                        writer,
4104                        sheet_name="Bond payments calendar",
4105                        index=False,
4106                        encoding="UTF-8",
4107                        freeze_panes=(1, 2),
4108                    )  # saving as XLSX-file with freeze first row and column as headers
4109
4110                    del humanReadable  # release df in memory
4111
4112                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4113
4114        return calendar
4115
4116    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4117        """
4118        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4119        Also, creates Markdown file with calendar data, `calendar.md` by default.
4120
4121        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4122
4123        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4124                        extended information about bonds: main info, current prices, bond payment calendar,
4125                        coupon yields, current yields and some statistics etc.
4126                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4127        :param show: if `True` then also printing bonds payment calendar to the console,
4128                     otherwise save to file `calendarFile` only. `False` by default.
4129        :return: multilines text in Markdown format with bonds payment calendar as a table.
4130        """
4131        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4132            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4133
4134        infoText = "# Bond payments calendar\n\n"
4135
4136        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4137
4138        if not (calendar is None or calendar.empty):
4139            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4140
4141            info = [
4142                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4143                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4144            ]
4145
4146            newMonth = False
4147            notOneBond = calendar["figi"].nunique() > 1
4148            for i, bond in enumerate(calendar.iterrows()):
4149                if newMonth and notOneBond:
4150                    info.append(splitLine)
4151
4152                info.append(
4153                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4154                        "  √" if bond[1]["paid"] else "  —",
4155                        bond[1]["couponDate"].split("T")[0],
4156                        bond[1]["figi"],
4157                        bond[1]["ticker"],
4158                        bond[1]["couponNumber"],
4159                        "{} {}".format(
4160                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4161                            bond[1]["payCurrency"],
4162                        ),
4163                        bond[1]["couponType"],
4164                        bond[1]["couponPeriod"],
4165                        bond[1]["fixDate"].split("T")[0],
4166                    )
4167                )
4168
4169                if i < len(calendar.values) - 1:
4170                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4171                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4172                    newMonth = False if curDate.month == nextDate.month else True
4173
4174                else:
4175                    newMonth = False
4176
4177            infoText += "".join(info)
4178
4179            if show:
4180                uLogger.info("{}".format(infoText))
4181
4182            if self.calendarFile is not None:
4183                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4184                    fH.write(infoText)
4185
4186                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4187
4188        else:
4189            infoText += "No data\n"
4190
4191        return infoText
4192
4193    def OverviewAccounts(self, show: bool = False) -> dict:
4194        """
4195        Method for parsing and show simple table with all available user accounts.
4196
4197        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4198
4199        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4200        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4201                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4202                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4203                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4204                                                        "closed": "—", "access": "Full access" }, ...}}`
4205        """
4206        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4207
4208        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4209        accounts = {
4210            item["id"]: {
4211                "type": TKS_ACCOUNT_TYPES[item["type"]],
4212                "name": item["name"],
4213                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4214                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4215                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4216                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4217            } for item in rawAccounts["accounts"]
4218        }
4219
4220        # Raw and parsed data with some fields replaced in "stat" section:
4221        view = {
4222            "rawAccounts": rawAccounts,
4223            "stat": accounts,
4224        }
4225
4226        # --- Prepare simple text table with only accounts data in human-readable format:
4227        if show:
4228            info = [
4229                "# User accounts\n\n",
4230                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4231                "| Account ID   | Type                      | Status                    | Name                           |\n",
4232                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4233            ]
4234
4235            for account in view["stat"].keys():
4236                info.extend([
4237                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4238                        account,
4239                        view["stat"][account]["type"],
4240                        view["stat"][account]["status"],
4241                        view["stat"][account]["name"],
4242                    )
4243                ])
4244
4245            infoText = "".join(info)
4246
4247            uLogger.info(infoText)
4248
4249            if self.userAccountsFile:
4250                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4251                    fH.write(infoText)
4252
4253                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4254
4255        return view
4256
4257    def OverviewUserInfo(self, show: bool = False) -> dict:
4258        """
4259        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4260
4261        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4262
4263        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4264        :return: dict with raw parsed data from server and some calculated statistics about it.
4265        """
4266        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4267        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4268        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4269        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4270        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4271        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4272
4273        # This is dict with parsed common user data:
4274        userInfo = {
4275            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4276            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4277            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4278            "tariff": rawUserInfo["tariff"],
4279        }
4280
4281        # This is an array of dict with parsed margin statuses for every account IDs:
4282        margins = {}
4283        for accountId in accounts.keys():
4284            if rawMargins[accountId]:
4285                margins[accountId] = {
4286                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4287                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4288                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4289                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4290                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4291                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4292                }
4293
4294            else:
4295                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4296
4297        unary = {}  # unary-connection limits
4298        for item in rawTariffLimits["unaryLimits"]:
4299            if item["limitPerMinute"] in unary.keys():
4300                unary[item["limitPerMinute"]].extend(item["methods"])
4301
4302            else:
4303                unary[item["limitPerMinute"]] = item["methods"]
4304
4305        stream = {}  # stream-connection limits
4306        for item in rawTariffLimits["streamLimits"]:
4307            if item["limit"] in stream.keys():
4308                stream[item["limit"]].extend(item["streams"])
4309
4310            else:
4311                stream[item["limit"]] = item["streams"]
4312
4313        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4314        limits = {
4315            "unary": unary,
4316            "stream": stream,
4317        }
4318
4319        # Raw and parsed data as an output result:
4320        view = {
4321            "rawUserInfo": rawUserInfo,
4322            "rawAccounts": rawAccounts,
4323            "rawMargins": rawMargins,
4324            "rawTariffLimits": rawTariffLimits,
4325            "stat": {
4326                "userInfo": userInfo,
4327                "accounts": accounts,
4328                "margins": margins,
4329                "limits": limits,
4330            },
4331        }
4332
4333        # --- Prepare text table with user information in human-readable format:
4334        if show:
4335            info = [
4336                "# Full user information\n\n",
4337                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4338                "## Common information\n\n",
4339                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4340                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4341                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4342                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4343                "\n## User accounts\n\n",
4344            ]
4345
4346            for account in view["stat"]["accounts"].keys():
4347                info.extend([
4348                    "### ID: [{}]\n\n".format(account),
4349                    "| Parameters           | Values                                                       |\n",
4350                    "|----------------------|--------------------------------------------------------------|\n",
4351                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4352                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4353                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4354                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4355                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4356                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4357                ])
4358
4359                if margins[account]:
4360                    info.extend([
4361                        "| Margin status:       | Enabled                                                      |\n",
4362                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4363                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4364                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4365                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4366                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4367                    ])
4368
4369                else:
4370                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4371
4372            info.extend([
4373                "\n## Current user tariff limits\n",
4374                "\nSee also:\n",
4375                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4376                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4377                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4378                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4379                "\n### Unary limits\n",
4380            ])
4381
4382            if unary:
4383                for key, values in sorted(unary.items()):
4384                    info.append("\n* Max requests per minute: {}\n".format(key))
4385
4386                    for value in values:
4387                        info.append("  - {}\n".format(value))
4388
4389            else:
4390                info.append("\nNot available\n")
4391
4392            info.append("\n### Stream limits\n")
4393
4394            if stream:
4395                for key, values in sorted(stream.items()):
4396                    info.append("\n* Max stream connections: {}\n".format(key))
4397
4398                    for value in values:
4399                        info.append("  - {}\n".format(value))
4400
4401            else:
4402                info.append("\nNot available\n")
4403
4404            infoText = "".join(info)
4405
4406            uLogger.info(infoText)
4407
4408            if self.userInfoFile:
4409                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4410                    fH.write(infoText)
4411
4412                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4413
4414        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
199        """
200        Main class init.
201
202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
205        :param useCache: use default cache file with raw data to use instead of `iList`.
206                         True by default. Cache is auto-update if new day has come.
207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
208        :param defaultCache: path to default cache file. `dump.json` by default.
209        """
210        if token is None or not token:
211            try:
212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
214
215            except KeyError:
216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
217                raise Exception("Token required")
218
219        else:
220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
222
223        if accountId is None or not accountId:
224            try:
225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
227
228            except KeyError:
229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
230
231        else:
232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
234
235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
237
238        Latest version: https://pypi.org/project/tksbrokerapi/
239        """
240
241        self.aliases = TKS_TICKER_ALIASES
242        """Some aliases instead official tickers.
243
244        See also: `TKSEnums.TKS_TICKER_ALIASES`
245        """
246
247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
248
249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
250
251        self.ticker = ""
252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
253
254        See also: `SearchByTicker()`, `SearchInstruments()`.
255        """
256
257        self.figi = ""
258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
259
260        See also: `SearchByFIGI()`, `SearchInstruments()`.
261        """
262
263        self.depth = 1
264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
265
266        See also: `GetCurrentPrices()`.
267        """
268
269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
271
272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
273        """
274
275        uLogger.debug("Broker API server: {}".format(self.server))
276
277        self.timeout = 15
278        """Server operations timeout in seconds. Default: `15`.
279
280        See also: `SendAPIRequest()`.
281        """
282
283        self.headers = {
284            "Content-Type": "application/json",
285            "accept": "application/json",
286            "Authorization": "Bearer {}".format(self.token),
287            "x-app-name": "Tim55667757.TKSBrokerAPI",
288        }
289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
290
291        See also: `SendAPIRequest()`.
292        """
293
294        self.body = None
295        """Request body which send to broker server. Default: `None`.
296
297        See also: `SendAPIRequest()`.
298        """
299
300        self.moreDebug = False
301        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
302
303        self.historyFile = None
304        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
305
306        See also: `History()`.
307        """
308
309        self.htmlHistoryFile = "index.html"
310        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
311
312        See also: `ShowHistoryChart()`.
313        """
314
315        self.instrumentsFile = "instruments.md"
316        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
317
318        See also: `ShowInstrumentsInfo()`.
319        """
320
321        self.searchResultsFile = "search-results.md"
322        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
323
324        See also: `SearchInstruments()`.
325        """
326
327        self.pricesFile = "prices.md"
328        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
329
330        See also: `GetListOfPrices()`.
331        """
332
333        self.infoFile = "info.md"
334        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
335
336        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
337        """
338
339        self.bondsXLSXFile = "ext-bonds.xlsx"
340        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
341        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
342
343        See also: `ExtendBondsData()`.
344        """
345
346        self.calendarFile = "calendar.md"
347        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
348        
349        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
350
351        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
352        """
353
354        self.overviewFile = "overview.md"
355        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
356
357        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
358        """
359
360        self.overviewDigestFile = "overview-digest.md"
361        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
362
363        See also: `Overview()` with parameter `details="digest"`.
364        """
365
366        self.overviewPositionsFile = "overview-positions.md"
367        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
368
369        See also: `Overview()` with parameter `details="positions"`.
370        """
371
372        self.overviewOrdersFile = "overview-orders.md"
373        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
374
375        See also: `Overview()` with parameter `details="orders"`.
376        """
377
378        self.overviewAnalyticsFile = "overview-analytics.md"
379        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
380
381        See also: `Overview()` with parameter `details="analytics"`.
382        """
383
384        self.reportFile = "deals.md"
385        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
386
387        See also: `Deals()`.
388        """
389
390        self.withdrawalLimitsFile = "limits.md"
391        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
392
393        See also: `OverviewLimits()` and `RequestLimits()`.
394        """
395
396        self.userInfoFile = "user-info.md"
397        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
398
399        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
400        """
401
402        self.userAccountsFile = "accounts.md"
403        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
404
405        See also: `OverviewAccounts()`, `RequestAccounts()`.
406        """
407
408        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
409        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
410
411        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
412
413        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
414        """
415
416        self.iList = None  # init iList for raw instruments data
417        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
418        
419        See also: `Listing()`, `DumpInstruments()`.
420        """
421
422        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
423        if useCache:
424            if os.path.exists(self.iListDumpFile):
425                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
426                curTime = datetime.now(tzutc())
427
428                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
429                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
430
431                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
432
433                else:
434                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
435
436                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
437                        os.path.abspath(self.iListDumpFile),
438                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
439                    ))
440
441            else:
442                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
443                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
444
445        else:
446            self.iList = self.Listing()  # request new raw instruments data from broker server
447            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
448
449        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
450        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
451
452        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
453        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
469    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
470        """
471        Send GET or POST request to broker server and receive JSON object.
472
473        self.header: must be defining with dictionary of headers.
474        self.body: if define then used as request body. None by default.
475        self.timeout: global request timeout, 15 seconds by default.
476        :param url: url with REST request.
477        :param reqType: send "GET" or "POST" request. "GET" by default.
478        :param retry: how many times retry after first request if an 5xx server errors occurred.
479        :param pause: sleep time in seconds between retries.
480        :return: response JSON (dictionary) from broker.
481        """
482        if reqType not in ("GET", "POST"):
483            uLogger.error("You can define request type: 'GET' or 'POST'!")
484            raise Exception("Incorrect value")
485
486        if self.moreDebug:
487            uLogger.debug("Request parameters:")
488            uLogger.debug("    - REST API URL: {}".format(url))
489            uLogger.debug("    - request type: {}".format(reqType))
490            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
491            uLogger.debug("    - body:\n{}".format(self.body))
492
493        # fast hack to avoid all operations with some tickers/FIGI
494        responseJSON = {}
495        oK = True
496        for item in self.exclude:
497            if item in url:
498                if self.moreDebug:
499                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
500
501                oK = False
502                break
503
504        if oK:
505            counter = 0
506            response = None
507            errMsg = ""
508
509            while not response and counter <= retry:
510                if reqType == "GET":
511                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
512
513                if reqType == "POST":
514                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
515
516                if self.moreDebug:
517                    uLogger.debug("Response:")
518                    uLogger.debug("    - status code: {}".format(response.status_code))
519                    uLogger.debug("    - reason: {}".format(response.reason))
520                    uLogger.debug("    - body length: {}".format(len(response.text)))
521                    uLogger.debug("    - headers:\n{}".format(response.headers))
522
523                # Server returns some headers:
524                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
525                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
526                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
527                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
528                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
529                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
530                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
531                    sleep(rateLimitWait)
532
533                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
534                if 400 <= response.status_code < 500:
535                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
536                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
537                    counter = retry + 1
538
539                if 500 <= response.status_code < 600:
540                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
541                    uLogger.debug("    - not oK, {}".format(errMsg))
542                    counter += 1
543
544                    if counter <= retry:
545                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
546                        sleep(pause)
547
548            responseJSON = self._ParseJSON(rawData=response.text)
549
550            if errMsg:
551                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
552                uLogger.error("    - not oK, {}".format(errMsg))
553
554        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
587    def Listing(self) -> dict:
588        """
589        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
590
591        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
592        """
593        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
594        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
595
596        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
597        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
598        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
599
600        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
601        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
602        poolUpdater.close()
603
604        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
605        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
606        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
607
608        # calculate minimum price increment (step) for all instruments and set up instrument's type:
609        for iType in iList.keys():
610            for ticker in iList[iType]:
611                iList[iType][ticker]["type"] = iType
612
613                if "minPriceIncrement" in iList[iType][ticker].keys():
614                    iList[iType][ticker]["step"] = NanoToFloat(
615                        iList[iType][ticker]["minPriceIncrement"]["units"],
616                        iList[iType][ticker]["minPriceIncrement"]["nano"],
617                    )
618
619                else:
620                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
621
622        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
624    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
625        """
626        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
627
628        See also: `DumpInstruments()`, `Listing()`.
629
630        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
631                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
632        """
633        if self.iListDumpFile is None or not self.iListDumpFile:
634            uLogger.error("Output name of dump file must be defined!")
635            raise Exception("Filename required")
636
637        if not self.iList or forceUpdate:
638            self.iList = self.Listing()
639
640        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
641
642        # Save as XLSX with separated sheets for every type of instruments:
643        with pd.ExcelWriter(
644                path=xlsxDumpFile,
645                date_format=TKS_DATE_FORMAT,
646                datetime_format=TKS_DATE_TIME_FORMAT,
647                mode="w",
648        ) as writer:
649            for iType in TKS_INSTRUMENTS:
650                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
651                df = df[sorted(df)]  # sorted by column names
652                df = df.applymap(
653                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
654                    na_action="ignore",
655                )  # converting numbers from nano-type to float in every cell
656                df.to_excel(
657                    writer,
658                    sheet_name=iType,
659                    encoding="UTF-8",
660                    freeze_panes=(1, 1),
661                )  # saving as XLSX-file with freeze first row and column as headers
662
663        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
665    def DumpInstruments(self, forceUpdate: bool = True) -> str:
666        """
667        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
668        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
669
670        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
671
672        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
673                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
674        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
675        """
676        if self.iListDumpFile is None or not self.iListDumpFile:
677            uLogger.error("Output name of dump file must be defined!")
678            raise Exception("Filename required")
679
680        if not self.iList or forceUpdate:
681            self.iList = self.Listing()
682
683        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
684        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
685            fH.write(jsonDump)
686
687        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
688
689        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
691    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
692        """
693        Show information about one instrument defined by json data and prints it in Markdown format.
694
695        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
696
697        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
698        :param show: if `True` then also printing information about instrument and its current price.
699        :return: multilines text in Markdown format with information about one instrument.
700        """
701        splitLine = "|                                                             |                                                        |\n"
702        infoText = ""
703
704        if iJSON is not None and iJSON and isinstance(iJSON, dict):
705            info = [
706                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
707                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
708                "| Parameters                                                  | Values                                                 |\n",
709                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
710                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
711                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
712            ]
713
714            if "sector" in iJSON.keys() and iJSON["sector"]:
715                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
716
717            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
718                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
719                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
720            )))
721
722            info.extend([
723                splitLine,
724                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
725                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
726            ])
727
728            if "isin" in iJSON.keys() and iJSON["isin"]:
729                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
730
731            if "classCode" in iJSON.keys():
732                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
733
734            info.extend([
735                splitLine,
736                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
737                splitLine,
738                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
739                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
740                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
741            ])
742
743            if iJSON["figi"]:
744                self.figi = iJSON["figi"]
745                iJSON = iJSON | self.RequestTradingStatus()
746
747                info.extend([
748                    splitLine,
749                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
750                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
751                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
752                ])
753
754            info.append(splitLine)
755
756            if "type" in iJSON.keys() and iJSON["type"]:
757                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
758
759            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
760                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
761
762            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
763                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
764
765            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
766                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
767
768            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
769                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
770
771            if "focusType" in iJSON.keys() and iJSON["focusType"]:
772                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
773
774            if "assetType" in iJSON.keys() and iJSON["assetType"]:
775                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
776
777            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
778                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
779
780            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
781                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
782
783            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
784                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
785
786            if "currency" in iJSON.keys():
787                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
788
789            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
790                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
791
792            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
793                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
794
795            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
796                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
797
798            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
799                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
800
801            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
802                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
803
804            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
805                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
806
807            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
808                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
809
810            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
811                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
812
813            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
814                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
815
816            iExt = None
817            if iJSON["type"] == "Bonds":
818                info.extend([
819                    splitLine,
820                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
821                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
822                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
823                        iJSON["nominal"]["currency"],
824                    )),
825                ])
826
827                if "floatingCouponFlag" in iJSON.keys():
828                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
829
830                if "amortizationFlag" in iJSON.keys():
831                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
832
833                info.append(splitLine)
834
835                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
836                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
837
838                if iJSON["figi"]:
839                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
840
841                    info.extend([
842                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
843                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
844                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
845                    ])
846
847                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
848                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
849                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
850                        iJSON["aciValue"]["currency"]
851                    )))
852
853            if "currentPrice" in iJSON.keys():
854                info.append(splitLine)
855
856                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
857                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
858
859                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
860                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
861                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
862                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
863                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
864
865                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
866                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
867
868                info.extend([
869                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
870                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
871                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
872                    )),
873                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
874                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
875                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
876                    )),
877                    "| Changes between last deal price and last close              | {:<54} |\n".format(
878                        "{:.2f}%{}".format(
879                            iJSON["currentPrice"]["changes"],
880                            " ({}{:.2f} {})".format(
881                                "+" if bondChangesDelta > 0 else "",
882                                bondChangesDelta,
883                                aciCurrency
884                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
885                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
886                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
887                                currency
888                            ),
889                        )
890                    ),
891                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
892                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
893                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
894                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
896                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
897                    )),
898                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
899                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
900                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
901                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
902                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
903                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
904                    )),
905                ])
906
907            if "lot" in iJSON.keys():
908                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
909
910            if "step" in iJSON.keys() and iJSON["step"] != 0:
911                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
912
913            # Add bond payment calendar:
914            if iJSON["type"] == "Bonds":
915                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
916                info.extend(["\n", strCalendar])
917
918            infoText += "".join(info)
919
920            if show:
921                uLogger.info("{}".format(infoText))
922
923            else:
924                uLogger.debug("{}".format(infoText))
925
926            if self.infoFile is not None:
927                with open(self.infoFile, "w", encoding="UTF-8") as fH:
928                    fH.write(infoText)
929
930                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
931
932        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 934    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 935        """
 936        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 937
 938        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 939        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 940        :return: JSON formatted data with information about instrument.
 941        """
 942        tickerJSON = {}
 943        if self.moreDebug:
 944            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 945
 946        if not self.ticker:
 947            uLogger.warning("self.ticker variable is not be empty!")
 948
 949        else:
 950            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 951                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 952                raise Exception("Instrument not allowed")
 953
 954            if not self.iList:
 955                self.iList = self.Listing()
 956
 957            if self.ticker in self.iList["Shares"].keys():
 958                tickerJSON = self.iList["Shares"][self.ticker]
 959                if self.moreDebug:
 960                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 961
 962            elif self.ticker in self.iList["Currencies"].keys():
 963                tickerJSON = self.iList["Currencies"][self.ticker]
 964                if self.moreDebug:
 965                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 966
 967            elif self.ticker in self.iList["Bonds"].keys():
 968                tickerJSON = self.iList["Bonds"][self.ticker]
 969                if self.moreDebug:
 970                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 971
 972            elif self.ticker in self.iList["Etfs"].keys():
 973                tickerJSON = self.iList["Etfs"][self.ticker]
 974                if self.moreDebug:
 975                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 976
 977            elif self.ticker in self.iList["Futures"].keys():
 978                tickerJSON = self.iList["Futures"][self.ticker]
 979                if self.moreDebug:
 980                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 981
 982        if tickerJSON:
 983            self.figi = tickerJSON["figi"]
 984
 985            if requestPrice:
 986                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 987
 988                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 989                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 990
 991                else:
 992                    tickerJSON["currentPrice"]["changes"] = 0
 993
 994            if show:
 995                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 996
 997        else:
 998            if show:
 999                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1000
1001        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1003    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1004        """
1005        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1006
1007        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1008        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1009        :return: JSON formatted data with information about instrument.
1010        """
1011        figiJSON = {}
1012        if self.moreDebug:
1013            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1014
1015        if not self.figi:
1016            uLogger.warning("self.figi variable is not be empty!")
1017
1018        else:
1019            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1020                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1021                raise Exception("Instrument not allowed")
1022
1023            if not self.iList:
1024                self.iList = self.Listing()
1025
1026            for item in self.iList["Shares"].keys():
1027                if self.figi == self.iList["Shares"][item]["figi"]:
1028                    figiJSON = self.iList["Shares"][item]
1029
1030                    if self.moreDebug:
1031                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1032
1033                    break
1034
1035            if not figiJSON:
1036                for item in self.iList["Currencies"].keys():
1037                    if self.figi == self.iList["Currencies"][item]["figi"]:
1038                        figiJSON = self.iList["Currencies"][item]
1039
1040                        if self.moreDebug:
1041                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1042
1043                        break
1044
1045            if not figiJSON:
1046                for item in self.iList["Bonds"].keys():
1047                    if self.figi == self.iList["Bonds"][item]["figi"]:
1048                        figiJSON = self.iList["Bonds"][item]
1049
1050                        if self.moreDebug:
1051                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1052
1053                        break
1054
1055            if not figiJSON:
1056                for item in self.iList["Etfs"].keys():
1057                    if self.figi == self.iList["Etfs"][item]["figi"]:
1058                        figiJSON = self.iList["Etfs"][item]
1059
1060                        if self.moreDebug:
1061                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1062
1063                        break
1064
1065            if not figiJSON:
1066                for item in self.iList["Futures"].keys():
1067                    if self.figi == self.iList["Futures"][item]["figi"]:
1068                        figiJSON = self.iList["Futures"][item]
1069
1070                        if self.moreDebug:
1071                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1072
1073                        break
1074
1075        if figiJSON:
1076            self.figi = figiJSON["figi"]
1077            self.ticker = figiJSON["ticker"]
1078
1079            if requestPrice:
1080                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1081
1082                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1083                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1084
1085                else:
1086                    figiJSON["currentPrice"]["changes"] = 0
1087
1088            if show:
1089                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1090
1091        else:
1092            if show:
1093                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1094
1095        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1097    def GetCurrentPrices(self, show: bool = True) -> dict:
1098        """
1099        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1100        `{"buy": [{"price": 1243.8, "quantity": 193},
1101                  {"price": 1244.0, "quantity": 168},
1102                  {"price": 1244.8, "quantity": 5},
1103                  {"price": 1245.0, "quantity": 61},
1104                  {"price": 1245.4, "quantity": 60}],
1105          "sell": [{"price": 1243.6, "quantity": 8},
1106                   {"price": 1242.6, "quantity": 10},
1107                   {"price": 1242.4, "quantity": 18},
1108                   {"price": 1242.2, "quantity": 50},
1109                   {"price": 1242.0, "quantity": 113}],
1110          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1111        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1112        - sell: list of dicts with Buyers prices,
1113            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1114            - quantity: volume value by current price in lots,
1115        - limitUp: current trade session limit price, maximum,
1116        - limitDown: current trade session limit price, minimum,
1117        - lastPrice: last deal price of the instrument,
1118        - closePrice: previous trade session close price of the instrument.
1119
1120        See also: `SearchByTicker()` and `SearchByFIGI()`.
1121        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1122        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1123
1124        :param show: if `True` then print DOM to log and console.
1125        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1126                 If an error occurred then returns an empty record:
1127                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1128        """
1129        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1130
1131        if self.depth < 1:
1132            uLogger.error("Depth of Market (DOM) must be >=1!")
1133            raise Exception("Incorrect value")
1134
1135        if not (self.ticker or self.figi):
1136            uLogger.error("self.ticker or self.figi variables must be defined!")
1137            raise Exception("Ticker or FIGI required")
1138
1139        if self.ticker and not self.figi:
1140            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1141            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1142
1143        if not self.ticker and self.figi:
1144            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1145            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1146
1147        if not self.figi:
1148            uLogger.error("FIGI is not defined!")
1149            raise Exception("Ticker or FIGI required")
1150
1151        else:
1152            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1153
1154            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1155            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1156            self.body = str({"figi": self.figi, "depth": self.depth})
1157            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1158
1159            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1160                # list of dicts with sellers orders:
1161                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1162
1163                # list of dicts with buyers orders:
1164                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1165
1166                # max price of instrument at this time:
1167                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1168
1169                # min price of instrument at this time:
1170                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1171
1172                # last price of deal with instrument:
1173                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1174
1175                # last close price of instrument:
1176                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1177
1178            else:
1179                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1180                uLogger.debug("Server response: {}".format(pricesResponse))
1181
1182            if show:
1183                if prices["buy"] or prices["sell"]:
1184                    info = [
1185                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1186                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1187                            self.ticker,
1188                            self.figi,
1189                            self.depth,
1190                        ),
1191                        "-" * 60, "\n",
1192                        "             Orders of Buyers | Orders of Sellers\n",
1193                        "-" * 60, "\n",
1194                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1195                        "-" * 60, "\n",
1196                    ]
1197
1198                    if not prices["buy"]:
1199                        info.append("                              | No orders!\n")
1200                        sumBuy = 0
1201
1202                    else:
1203                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1204                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1205                        for item in maxMinSorted:
1206                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1207
1208                    if not prices["sell"]:
1209                        info.append("No orders!                    |\n")
1210                        sumSell = 0
1211
1212                    else:
1213                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1214                        for item in prices["sell"]:
1215                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1216
1217                    info.extend([
1218                        "-" * 60, "\n",
1219                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1220                        "-" * 60, "\n",
1221                    ])
1222
1223                    infoText = "".join(info)
1224
1225                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1226
1227                else:
1228                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1229
1230        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1232    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1233        """
1234        This method get and show information about all available broker instruments for current user account.
1235        If `instrumentsFile` string is not empty then also save information to this file.
1236
1237        :param show: if `True` then print results to console, if `False` - print only to file.
1238        :return: multi-lines string with all available broker instruments
1239        """
1240        if not self.iList:
1241            self.iList = self.Listing()
1242
1243        info = [
1244            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1245            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1246        ]
1247
1248        # add instruments count by type:
1249        for iType in self.iList.keys():
1250            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1251
1252        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1253        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1254
1255        # generating info tables with all instruments by type:
1256        for iType in self.iList.keys():
1257            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1258
1259            for instrument in self.iList[iType].keys():
1260                iName = self.iList[iType][instrument]["name"]  # instrument's name
1261                if len(iName) > 57:
1262                    iName = "{}...".format(iName[:54])  # right trim for a long string
1263
1264                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1265                    self.iList[iType][instrument]["ticker"],
1266                    iName,
1267                    self.iList[iType][instrument]["figi"],
1268                    self.iList[iType][instrument]["currency"],
1269                    self.iList[iType][instrument]["lot"],
1270                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1271                ))
1272
1273        infoText = "".join(info)
1274
1275        if show:
1276            uLogger.info(infoText)
1277
1278        if self.instrumentsFile:
1279            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1280                fH.write(infoText)
1281
1282            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1283
1284        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False - print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1286    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1287        """
1288        This method search and show information about instruments by part of its ticker, FIGI or name.
1289        If `searchResultsFile` string is not empty then also save information to this file.
1290
1291        :param pattern: string with part of ticker, FIGI or instrument's name.
1292        :param show: if `True` then print results to console, if `False` - return list of result only.
1293        :return: list of dictionaries with all found instruments.
1294        """
1295        if not self.iList:
1296            self.iList = self.Listing()
1297
1298        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1299        compiledPattern = re.compile(pattern, re.IGNORECASE)
1300
1301        for iType in self.iList:
1302            for instrument in self.iList[iType].values():
1303                searchResult = compiledPattern.search(" ".join(
1304                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1305                ))
1306
1307                if searchResult:
1308                    searchResults[iType][instrument["ticker"]] = instrument
1309
1310        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1311        info = [
1312            "# Search results\n\n",
1313            "* **Search pattern:** [{}]\n".format(pattern),
1314            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1315            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1316        ]
1317        infoShort = info[:]
1318
1319        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1320        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1321        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1322
1323        if resultsLen == 0:
1324            info.append("\nNo results\n")
1325            infoShort.append("\nNo results\n")
1326            uLogger.warning("No results. Try changing your search pattern.")
1327
1328        else:
1329            for iType in searchResults:
1330                iTypeValuesCount = len(searchResults[iType].values())
1331                if iTypeValuesCount > 0:
1332                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1333                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1334
1335                    for instrument in searchResults[iType].values():
1336                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1337                            instrument["type"],
1338                            instrument["ticker"],
1339                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1340                            instrument["figi"],
1341                        ))
1342
1343                    if iTypeValuesCount <= 5:
1344                        infoShort.extend(info[-iTypeValuesCount:])
1345
1346                    else:
1347                        infoShort.extend(info[-5:])
1348                        infoShort.append(skippedLine)
1349
1350        infoText = "".join(info)
1351        infoTextShort = "".join(infoShort)
1352
1353        if show:
1354            uLogger.info(infoTextShort)
1355            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1356
1357        if self.searchResultsFile:
1358            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1359                fH.write(infoText)
1360
1361            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1362
1363        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False - return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1365    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1366        """
1367        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1368
1369        :param instruments: list of strings with tickers or FIGIs.
1370        :return: list with unique instrument FIGIs only.
1371        """
1372        requestedInstruments = []
1373        for iName in instruments:
1374            if iName not in self.aliases.keys():
1375                if iName not in requestedInstruments:
1376                    requestedInstruments.append(iName)
1377
1378            else:
1379                if iName not in requestedInstruments:
1380                    if self.aliases[iName] not in requestedInstruments:
1381                        requestedInstruments.append(self.aliases[iName])
1382
1383        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1384
1385        onlyUniqueFIGIs = []
1386        for iName in requestedInstruments:
1387            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1388                continue
1389
1390            self.ticker = iName
1391            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1392
1393            if not iData:
1394                self.ticker = ""
1395                self.figi = iName
1396
1397                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1398
1399                if not iData:
1400                    self.figi = ""
1401                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1402
1403            if iData and iData["figi"] not in onlyUniqueFIGIs:
1404                onlyUniqueFIGIs.append(iData["figi"])
1405
1406        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1407
1408        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1410    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1411        """
1412        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1413        See limits: https://tinkoff.github.io/investAPI/limits/
1414        If `pricesFile` string is not empty then also save information to this file.
1415
1416        :param instruments: list of strings with tickers or FIGIs.
1417        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1418        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1419                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1420        """
1421        if instruments is None or not instruments:
1422            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1423            raise Exception("Ticker or FIGI required")
1424
1425        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1426
1427        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1428
1429        iList = []  # trying to get info and current prices about all unique instruments:
1430        for self.figi in onlyUniqueFIGIs:
1431            iData = self.SearchByFIGI(requestPrice=True)
1432            iList.append(iData)
1433
1434        self.ShowListOfPrices(iList, show)
1435
1436        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! See limits: https://tinkoff.github.io/investAPI/limits/ If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1438    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1439        """
1440        Show table contains current prices of given instruments.
1441
1442        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1443                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1444        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1445        :return: multilines text in Markdown format as a table contains current prices.
1446        """
1447        infoText = ""
1448
1449        if show or self.pricesFile:
1450            info = [
1451                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1452                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1453                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1454            ]
1455
1456            for item in iList:
1457                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1458                    item["ticker"],
1459                    item["figi"],
1460                    item["type"],
1461                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1462                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1463                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1464                    "{} / {}".format(
1465                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1466                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1467                    ),
1468                    "{} / {}".format(
1469                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1470                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1471                    ),
1472                    item["currency"],
1473                ))
1474
1475            infoText = "".join(info)
1476
1477            if show:
1478                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1479
1480            if self.pricesFile:
1481                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1482                    fH.write(infoText)
1483
1484                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1485
1486        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1488    def RequestTradingStatus(self) -> dict:
1489        """
1490        Requesting trading status for the instrument defined by `figi` variable.
1491        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1492        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1493
1494        :return: dictionary with trading status attributes. Response example:
1495                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1496                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1497        """
1498        if self.figi is None or not self.figi:
1499            uLogger.error("Variable `figi` must be defined for using this method!")
1500            raise Exception("FIGI required")
1501
1502        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1503
1504        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1505        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1506        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1507
1508        if self.moreDebug:
1509            uLogger.debug("Records about current trading status successfully received")
1510
1511        return tradingStatus

Requesting trading status for the instrument defined by figi variable. REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1513    def RequestPortfolio(self) -> dict:
1514        """
1515        Requesting actual user's portfolio for current `accountId`.
1516        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1517        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1518
1519        :return: dictionary with user's portfolio.
1520        """
1521        if self.accountId is None or not self.accountId:
1522            uLogger.error("Variable `accountId` must be defined for using this method!")
1523            raise Exception("Account ID required")
1524
1525        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1526
1527        self.body = str({"accountId": self.accountId})
1528        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1529        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1530
1531        if self.moreDebug:
1532            uLogger.debug("Records about user's portfolio successfully received")
1533
1534        return rawPortfolio

Requesting actual user's portfolio for current accountId. REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1536    def RequestPositions(self) -> dict:
1537        """
1538        Requesting open positions by currencies and instruments for current `accountId`.
1539        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1540        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1541
1542        :return: dictionary with open positions by instruments.
1543        """
1544        if self.accountId is None or not self.accountId:
1545            uLogger.error("Variable `accountId` must be defined for using this method!")
1546            raise Exception("Account ID required")
1547
1548        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1549
1550        self.body = str({"accountId": self.accountId})
1551        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1552        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1553
1554        if self.moreDebug:
1555            uLogger.debug("Records about current open positions successfully received")
1556
1557        return rawPositions

Requesting open positions by currencies and instruments for current accountId. REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1559    def RequestPendingOrders(self) -> list:
1560        """
1561        Requesting current actual pending orders for current `accountId`.
1562        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1563        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1564
1565        :return: list of dictionaries with pending orders.
1566        """
1567        if self.accountId is None or not self.accountId:
1568            uLogger.error("Variable `accountId` must be defined for using this method!")
1569            raise Exception("Account ID required")
1570
1571        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1572
1573        self.body = str({"accountId": self.accountId})
1574        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1575        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1576
1577        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1578
1579        return rawOrders

Requesting current actual pending orders for current accountId. REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1581    def RequestStopOrders(self) -> list:
1582        """
1583        Requesting current actual stop orders for current `accountId`.
1584        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1585        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1586
1587        :return: list of dictionaries with stop orders.
1588        """
1589        if self.accountId is None or not self.accountId:
1590            uLogger.error("Variable `accountId` must be defined for using this method!")
1591            raise Exception("Account ID required")
1592
1593        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1594
1595        self.body = str({"accountId": self.accountId})
1596        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1597        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1598
1599        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1600
1601        return rawStopOrders

Requesting current actual stop orders for current accountId. REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1603    def Overview(self, show: bool = False, details: str = "full") -> dict:
1604        """
1605        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1606        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1607        are defined then also save information to file.
1608
1609        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1610        many requests about the state of the portfolio, and then, based on the received data, a large number
1611        of calculation and statistics are collected.
1612
1613        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1614        :param details: how detailed should the information be? You should specify one of strings:
1615                        `full` - shows full available information about portfolio status (by default),
1616                        `positions` - shows only open positions,
1617                        `digest` - show a short digest of the portfolio status,
1618                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1619                        `orders` - shows only sections of open limits and stop orders.
1620        :return: dictionary with client's raw portfolio and some statistics.
1621        """
1622        if self.accountId is None or not self.accountId:
1623            uLogger.error("Variable `accountId` must be defined for using this method!")
1624            raise Exception("Account ID required")
1625
1626        view = {
1627            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1628                "headers": {},  # list of dictionaries, response headers without "positions" section
1629                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1630                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1631                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1632                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1633                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1634                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1635                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1636                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1637                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1638            },
1639            "stat": {  # --- some statistics calculated using "raw" sections:
1640                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1641                "availableRUB": 0.,  # available rubles (without other currencies)
1642                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1643                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1644                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1645                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1646                "sharesCostRUB": 0.,  # costs of all shares in RUB
1647                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1648                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1649                "futuresCostRUB": 0.,  # costs of all futures in RUB
1650                "Currencies": [],  # list of dictionaries of all currencies statistics
1651                "Shares": [],  # list of dictionaries of all shares statistics
1652                "Bonds": [],  # list of dictionaries of all bonds statistics
1653                "Etfs": [],  # list of dictionaries of all etfs statistics
1654                "Futures": [],  # list of dictionaries of all futures statistics
1655                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1656                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1657                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1658                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1659                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1660            },
1661            "analytics": {  # --- some analytics of portfolio:
1662                "distrByAssets": {},  # portfolio distribution by assets
1663                "distrByCompanies": {},  # portfolio distribution by companies
1664                "distrBySectors": {},  # portfolio distribution by sectors
1665                "distrByCurrencies": {},  # portfolio distribution by currencies
1666                "distrByCountries": {},  # portfolio distribution by countries
1667            }
1668        }
1669
1670        details = details.lower()
1671        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1672        if details not in availableDetails:
1673            details = "full"
1674            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1675
1676        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1677
1678        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1679        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1680        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1681        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1682
1683        # save response headers without "positions" section:
1684        for key in portfolioResponse.keys():
1685            if key != "positions":
1686                view["raw"]["headers"][key] = portfolioResponse[key]
1687
1688            else:
1689                continue
1690
1691        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1692        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1693        for item in portfolioResponse["positions"]:
1694            if item["instrumentType"] == "currency":
1695                self.figi = item["figi"]
1696                curr = self.SearchByFIGI(requestPrice=False)
1697
1698                # current price of currency in RUB:
1699                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1700                    "name": curr["name"],
1701                    "currentPrice": NanoToFloat(
1702                        item["currentPrice"]["units"],
1703                        item["currentPrice"]["nano"]
1704                    ),
1705                }
1706
1707                view["raw"]["Currencies"].append(item)
1708
1709            elif item["instrumentType"] == "share":
1710                view["raw"]["Shares"].append(item)
1711
1712            elif item["instrumentType"] == "bond":
1713                view["raw"]["Bonds"].append(item)
1714
1715            elif item["instrumentType"] == "etf":
1716                view["raw"]["Etfs"].append(item)
1717
1718            elif item["instrumentType"] == "futures":
1719                view["raw"]["Futures"].append(item)
1720
1721            else:
1722                continue
1723
1724        # how many volume of currencies (by ISO currency name) are blocked:
1725        for item in view["raw"]["positions"]["blocked"]:
1726            blocked = NanoToFloat(item["units"], item["nano"])
1727            if blocked > 0:
1728                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1729
1730        # how many volume of instruments (by FIGI) are blocked:
1731        for item in view["raw"]["positions"]["securities"]:
1732            blocked = int(item["blocked"])
1733            if blocked > 0:
1734                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1735
1736        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1737
1738        if "rub" in allBlocked.keys():
1739            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1740
1741        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1742        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1743        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1744        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1745        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1746        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1747        view["stat"]["portfolioCostRUB"] = sum([
1748            view["stat"]["allCurrenciesCostRUB"],
1749            view["stat"]["sharesCostRUB"],
1750            view["stat"]["bondsCostRUB"],
1751            view["stat"]["etfsCostRUB"],
1752            view["stat"]["futuresCostRUB"],
1753        ])
1754
1755        # --- calculating some portfolio statistics:
1756        byComp = {}  # distribution by companies
1757        bySect = {}  # distribution by sectors
1758        byCurr = {}  # distribution by currencies (include RUB)
1759        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1760        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1761
1762        for item in portfolioResponse["positions"]:
1763            self.figi = item["figi"]
1764            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1765
1766            if instrument:
1767                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1768                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1769
1770                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1771                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1772
1773                else:
1774                    blocked = 0
1775
1776                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1777                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1778                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1779                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1780                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1781                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1782                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1783                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1784                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1785                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1786                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1787                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1788
1789                statData = {
1790                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1791                    "ticker": instrument["ticker"],  # ticker by FIGI
1792                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1793                    "volume": volume,  # available volume of instrument
1794                    "lots": lots,  # volume in lots of instrument
1795                    "direction": direction,  # direction of an instrument's position: short or long
1796                    "blocked": blocked,  # blocked volume of currency or instrument
1797                    "currentPrice": curPrice,  # current instrument's price in basic asset
1798                    "average": average,  # current average position price
1799                    "cost": cost,  # current cost of all volume of instrument in basic asset
1800                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1801                    "costRUB": costRUB,  # cost of instrument in ruble
1802                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1803                    "profit": profit,  # expected profit at current moment
1804                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1805                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1806                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1807                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1808                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1809                    "step": instrument["step"],  # minimum price increment
1810                }
1811
1812                # adding distribution by unique countries:
1813                if statData["country"] not in byCountry.keys():
1814                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1815
1816                else:
1817                    byCountry[statData["country"]]["cost"] += costRUB
1818                    byCountry[statData["country"]]["percent"] += percentCostRUB
1819
1820                if item["instrumentType"] != "currency":
1821                    # adding distribution by unique companies:
1822                    if statData["name"]:
1823                        if statData["name"] not in byComp.keys():
1824                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1825
1826                        else:
1827                            byComp[statData["name"]]["cost"] += costRUB
1828                            byComp[statData["name"]]["percent"] += percentCostRUB
1829
1830                    # adding distribution by unique sectors:
1831                    if statData["sector"] not in bySect.keys():
1832                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1833
1834                    else:
1835                        bySect[statData["sector"]]["cost"] += costRUB
1836                        bySect[statData["sector"]]["percent"] += percentCostRUB
1837
1838                # adding distribution by unique currencies:
1839                if currency not in byCurr.keys():
1840                    byCurr[currency] = {
1841                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1842                        "cost": costRUB,
1843                        "percent": percentCostRUB
1844                    }
1845
1846                else:
1847                    byCurr[currency]["cost"] += costRUB
1848                    byCurr[currency]["percent"] += percentCostRUB
1849
1850                # saving statistics for every instrument:
1851                if item["instrumentType"] == "currency":
1852                    view["stat"]["Currencies"].append(statData)
1853
1854                    # update dict with free funds for trading (total - blocked) by currencies
1855                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1856                    view["stat"]["funds"][currency] = {
1857                        "total": volume,
1858                        "totalCostRUB": costRUB,  # total volume cost in rubles
1859                        "free": volume - blocked,
1860                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1861                    }
1862
1863                elif item["instrumentType"] == "share":
1864                    view["stat"]["Shares"].append(statData)
1865
1866                elif item["instrumentType"] == "bond":
1867                    view["stat"]["Bonds"].append(statData)
1868
1869                elif item["instrumentType"] == "etf":
1870                    view["stat"]["Etfs"].append(statData)
1871
1872                elif item["instrumentType"] == "Futures":
1873                    view["stat"]["Futures"].append(statData)
1874
1875                else:
1876                    continue
1877
1878        # total changes in Russian Ruble:
1879        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1880        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1881        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1882        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1883        view["stat"]["funds"]["rub"] = {
1884            "total": view["stat"]["availableRUB"],
1885            "totalCostRUB": view["stat"]["availableRUB"],
1886            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1887            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1888        }
1889
1890        # --- pending orders sector data:
1891        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1892        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1893
1894        for item in view["raw"]["orders"]:
1895            self.figi = item["figi"]
1896
1897            if item["figi"] not in uniquePendingOrdersFIGIs:
1898                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1899
1900                uniquePendingOrdersFIGIs.append(item["figi"])
1901                uniquePendingOrders[item["figi"]] = instrument
1902
1903            else:
1904                instrument = uniquePendingOrders[item["figi"]]
1905
1906            if instrument:
1907                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1908                orderType = TKS_ORDER_TYPES[item["orderType"]]
1909                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1910                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1911
1912                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1913                if item["direction"] == "ORDER_DIRECTION_BUY":
1914                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1915
1916                else:
1917                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1918
1919                # requested price for order execution:
1920                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1921
1922                # necessary changes in percent to reach target from current price:
1923                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1924
1925                view["stat"]["orders"].append({
1926                    "orderID": item["orderId"],  # orderId number parameter of current order
1927                    "figi": item["figi"],  # FIGI identification
1928                    "ticker": instrument["ticker"],  # ticker name by FIGI
1929                    "lotsRequested": item["lotsRequested"],  # requested lots value
1930                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1931                    "currentPrice": lastPrice,  # current instrument's price for defined action
1932                    "targetPrice": target,  # requested price for order execution in base currency
1933                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1934                    "percentChanges": changes,  # changes in percent to target from current price
1935                    "currency": item["currency"],  # instrument's currency name
1936                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1937                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1938                    "status": orderState,  # order status from TKS_ORDER_STATES
1939                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1940                })
1941
1942        # --- stop orders sector data:
1943        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1944        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1945
1946        for item in view["raw"]["stopOrders"]:
1947            self.figi = item["figi"]
1948
1949            if item["figi"] not in uniqueStopOrdersFIGIs:
1950                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1951
1952                uniqueStopOrdersFIGIs.append(item["figi"])
1953                uniqueStopOrders[item["figi"]] = instrument
1954
1955            else:
1956                instrument = uniqueStopOrders[item["figi"]]
1957
1958            if instrument:
1959                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1960                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1961                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1962
1963                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1964                if "expirationTime" in item.keys():
1965                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1966                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1967
1968                else:
1969                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1970                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1971
1972                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1973                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1974                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1975
1976                else:
1977                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1978
1979                # requested price when stop-order executed:
1980                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1981
1982                # price for limit-order, set up when stop-order executed:
1983                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1984
1985                # necessary changes in percent to reach target from current price:
1986                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1987
1988                view["stat"]["stopOrders"].append({
1989                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1990                    "figi": item["figi"],  # FIGI identification
1991                    "ticker": instrument["ticker"],  # ticker name by FIGI
1992                    "lotsRequested": item["lotsRequested"],  # requested lots value
1993                    "currentPrice": lastPrice,  # current instrument's price for defined action
1994                    "targetPrice": target,  # requested price for stop-order execution in base currency
1995                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1996                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1997                    "percentChanges": changes,  # changes in percent to target from current price
1998                    "currency": item["currency"],  # instrument's currency name
1999                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2000                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2001                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2002                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2003                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2004                })
2005
2006        # --- calculating data for analytics section:
2007        # portfolio distribution by assets:
2008        view["analytics"]["distrByAssets"] = {
2009            "Ruble": {
2010                "uniques": 1,
2011                "cost": view["stat"]["availableRUB"],
2012                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2013            },
2014            "Currencies": {
2015                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2016                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2017                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2018            },
2019            "Shares": {
2020                "uniques": len(view["stat"]["Shares"]),
2021                "cost": view["stat"]["sharesCostRUB"],
2022                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2023            },
2024            "Bonds": {
2025                "uniques": len(view["stat"]["Bonds"]),
2026                "cost": view["stat"]["bondsCostRUB"],
2027                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2028            },
2029            "Etfs": {
2030                "uniques": len(view["stat"]["Etfs"]),
2031                "cost": view["stat"]["etfsCostRUB"],
2032                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2033            },
2034            "Futures": {
2035                "uniques": len(view["stat"]["Futures"]),
2036                "cost": view["stat"]["futuresCostRUB"],
2037                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038            },
2039        }
2040
2041        # portfolio distribution by companies:
2042        view["analytics"]["distrByCompanies"]["All money cash"] = {
2043            "ticker": "",
2044            "cost": view["stat"]["allCurrenciesCostRUB"],
2045            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2046        }
2047        view["analytics"]["distrByCompanies"].update(byComp)
2048
2049        # portfolio distribution by sectors:
2050        view["analytics"]["distrBySectors"]["All money cash"] = {
2051            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2052            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2053        }
2054        view["analytics"]["distrBySectors"].update(bySect)
2055
2056        # portfolio distribution by currencies:
2057        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2058            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2059
2060            if self.moreDebug:
2061                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2062
2063        view["analytics"]["distrByCurrencies"].update(byCurr)
2064        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2065        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2066
2067        # portfolio distribution by countries:
2068        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2069            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2070
2071            if self.moreDebug:
2072                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2073
2074        view["analytics"]["distrByCountries"].update(byCountry)
2075        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2076        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2077
2078        # --- Prepare text statistics overview in human-readable:
2079        if show:
2080            # Whatever the value `details`, header not changes:
2081            info = [
2082                "# Client's portfolio\n\n",
2083                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2084                "* **Account ID:** [{}]\n".format(self.accountId),
2085            ]
2086
2087            if details in ["full", "positions", "digest"]:
2088                info.extend([
2089                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2090                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2091                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2092                        view["stat"]["totalChangesRUB"],
2093                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2094                        view["stat"]["totalChangesPercentRUB"],
2095                    ),
2096                ])
2097
2098            if details in ["full", "positions"]:
2099                info.extend([
2100                    "## Open positions\n\n",
2101                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2102                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2103                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2104                        "{:.2f} ({:.2f}) rub".format(
2105                            view["stat"]["availableRUB"],
2106                            view["stat"]["blockedRUB"],
2107                        )
2108                    )
2109                ])
2110
2111                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2112                    return [
2113                        "|                             |                                 |          |              |              |                     |                              |\n",
2114                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2115                            noTradeStr if noTradeStr else typeStr,
2116                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2117                        ),
2118                    ]
2119
2120                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2121                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2122                        "{} [{}]".format(data["ticker"], data["figi"]),
2123                        "{:.2f} ({:.2f}) {}".format(
2124                            data["volume"],
2125                            data["blocked"],
2126                            data["currency"],
2127                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2128                            data["volume"],
2129                            data["blocked"],
2130                        ),
2131                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2132                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2133                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2134                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2135                        "{}{:.2f} {} ({}{:.2f}%)".format(
2136                            "+" if data["profit"] > 0 else "",
2137                            data["profit"], data["baseCurrencyName"],
2138                            "+" if data["percentProfit"] > 0 else "",
2139                            data["percentProfit"],
2140                        ),
2141                    )
2142
2143                # --- Show currencies section:
2144                if view["stat"]["Currencies"]:
2145                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2146                    for item in view["stat"]["Currencies"]:
2147                        info.append(_InfoStr(item, showCurrencyName=True))
2148
2149                else:
2150                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2151
2152                # --- Show shares section:
2153                if view["stat"]["Shares"]:
2154                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2155
2156                    for item in view["stat"]["Shares"]:
2157                        info.append(_InfoStr(item))
2158
2159                else:
2160                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2161
2162                # --- Show bonds section:
2163                if view["stat"]["Bonds"]:
2164                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2165
2166                    for item in view["stat"]["Bonds"]:
2167                        info.append(_InfoStr(item))
2168
2169                else:
2170                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2171
2172                # --- Show etfs section:
2173                if view["stat"]["Etfs"]:
2174                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2175
2176                    for item in view["stat"]["Etfs"]:
2177                        info.append(_InfoStr(item))
2178
2179                else:
2180                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2181
2182                # --- Show futures section:
2183                if view["stat"]["Futures"]:
2184                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2185
2186                    for item in view["stat"]["Futures"]:
2187                        info.append(_InfoStr(item))
2188
2189                else:
2190                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2191
2192            if details in ["full", "orders"]:
2193                # --- Show pending orders section:
2194                if view["stat"]["orders"]:
2195                    info.extend([
2196                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2197                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2198                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2199                    ])
2200
2201                    for item in view["stat"]["orders"]:
2202                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2203                            "{} [{}]".format(item["ticker"], item["figi"]),
2204                            item["orderID"],
2205                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2206                            "{} {} ({}{:.2f}%)".format(
2207                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2208                                item["baseCurrencyName"],
2209                                "+" if item["percentChanges"] > 0 else "",
2210                                float(item["percentChanges"]),
2211                            ),
2212                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2213                            item["action"],
2214                            item["type"],
2215                            item["date"],
2216                        ))
2217
2218                else:
2219                    info.append("\n## Total pending limit-orders: 0\n")
2220
2221                # --- Show stop orders section:
2222                if view["stat"]["stopOrders"]:
2223                    info.extend([
2224                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2225                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2226                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2227                    ])
2228
2229                    for item in view["stat"]["stopOrders"]:
2230                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2231                            "{} [{}]".format(item["ticker"], item["figi"]),
2232                            item["orderID"],
2233                            item["lotsRequested"],
2234                            "{} {} ({}{:.2f}%)".format(
2235                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2236                                item["baseCurrencyName"],
2237                                "+" if item["percentChanges"] > 0 else "",
2238                                float(item["percentChanges"]),
2239                            ),
2240                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2241                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2242                            item["action"],
2243                            item["type"],
2244                            item["expType"],
2245                            item["createDate"],
2246                            item["expDate"],
2247                        ))
2248
2249                else:
2250                    info.append("\n## Total stop-orders: 0\n")
2251
2252            if details in ["full", "analytics"]:
2253                # -- Show analytics section:
2254                if view["stat"]["portfolioCostRUB"] > 0:
2255                    info.extend([
2256                        "\n# Analytics\n"
2257                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2258                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2259                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2260                            view["stat"]["totalChangesRUB"],
2261                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2262                            view["stat"]["totalChangesPercentRUB"],
2263                        ),
2264                        "\n## Portfolio distribution by assets\n"
2265                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2266                        "|------------|---------|---------|--------------------|\n",
2267                    ])
2268
2269                    for key in view["analytics"]["distrByAssets"].keys():
2270                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2271                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2272                                key,
2273                                view["analytics"]["distrByAssets"][key]["uniques"],
2274                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2275                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2276                            ))
2277
2278                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2279                    info.extend([
2280                        "\n## Portfolio distribution by companies\n"
2281                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2282                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2283                    ])
2284
2285                    for company in view["analytics"]["distrByCompanies"].keys():
2286                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2287                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2288                            info.append("| {} | {:<7} | {:<18} |\n".format(
2289                                "{}{}{}".format(
2290                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2291                                    company,
2292                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2293                                ),
2294                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2295                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2296                            ))
2297
2298                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2299                    info.extend([
2300                        "\n## Portfolio distribution by sectors\n"
2301                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2302                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2303                    ])
2304
2305                    for sector in view["analytics"]["distrBySectors"].keys():
2306                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2307                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2308                                sector,
2309                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2310                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2311                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2312                            ))
2313
2314                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2315                    info.extend([
2316                        "\n## Portfolio distribution by currencies\n"
2317                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2318                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2319                    ])
2320
2321                    for curr in view["analytics"]["distrByCurrencies"].keys():
2322                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2323                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2324                            info.append("| {} | {:<7} | {:<18} |\n".format(
2325                                "[{}] {}{}".format(
2326                                    curr,
2327                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2328                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2329                                ),
2330                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2331                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2332                            ))
2333
2334                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2335                    info.extend([
2336                        "\n## Portfolio distribution by countries\n"
2337                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2338                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2339                    ])
2340
2341                    for country in view["analytics"]["distrByCountries"].keys():
2342                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2343                            nameLen = len(country)
2344                            info.append("| {} | {:<7} | {:<18} |\n".format(
2345                                "{}{}".format(
2346                                    country,
2347                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2348                                ),
2349                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2350                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2351                            ))
2352
2353            infoText = "".join(info)
2354
2355            uLogger.info(infoText)
2356
2357            if details == "full" and self.overviewFile:
2358                filename = self.overviewFile
2359
2360            elif details == "digest" and self.overviewDigestFile:
2361                filename = self.overviewDigestFile
2362
2363            elif details == "positions" and self.overviewPositionsFile:
2364                filename = self.overviewPositionsFile
2365
2366            elif details == "orders" and self.overviewOrdersFile:
2367                filename = self.overviewOrdersFile
2368
2369            elif details == "analytics" and self.overviewAnalyticsFile:
2370                filename = self.overviewAnalyticsFile
2371
2372            else:
2373                filename = ""
2374
2375            if filename:
2376                with open(filename, "w", encoding="UTF-8") as fH:
2377                    fH.write(infoText)
2378
2379                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2380
2381        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be? You should specify one of strings: full - shows full available information about portfolio status (by default), positions - shows only open positions, digest - show a short digest of the portfolio status, analytics - shows only the analytics section and the distribution of the portfolio by various categories, orders - shows only sections of open limits and stop orders.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2383    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2384        """
2385        Returns history operations between two given dates for current `accountId`.
2386        If `reportFile` string is not empty then also save human-readable report.
2387        Shows some statistical data of closed positions.
2388
2389        :param start: see docstring in `GetDatesAsString()` method
2390        :param end: see docstring in `GetDatesAsString()` method
2391        :param show: if `True` then also prints all records to the console.
2392        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2393        :return: original list of dictionaries with history of deals records from API ("operations" key):
2394                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2395                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2396        """
2397        if self.accountId is None or not self.accountId:
2398            uLogger.error("Variable `accountId` must be defined for using this method!")
2399            raise Exception("Account ID required")
2400
2401        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2402
2403        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2404
2405        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2406        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2407        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2408        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2409        customStat = {}  # custom statistics in additional to responseJSON
2410
2411        # --- output report in human-readable format:
2412        if show or self.reportFile:
2413            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2414            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2415            nextDay = ""
2416
2417            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2418
2419            if len(ops) > 0:
2420                customStat = {
2421                    "opsCount": 0,  # total operations count
2422                    "buyCount": 0,  # buy operations
2423                    "sellCount": 0,  # sell operations
2424                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2425                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2426                    "payIn": {"rub": 0.},  # Deposit brokerage account
2427                    "payOut": {"rub": 0.},  # Withdrawals
2428                    "divs": {"rub": 0.},  # Dividends income
2429                    "coupons": {"rub": 0.},  # Coupon's income
2430                    "brokerCom": {"rub": 0.},  # Service commissions
2431                    "serviceCom": {"rub": 0.},  # Service commissions
2432                    "marginCom": {"rub": 0.},  # Margin commissions
2433                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2434                }
2435
2436                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2437                for item in ops:
2438                    if item["state"] == "OPERATION_STATE_EXECUTED":
2439                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2440
2441                        # count buy operations:
2442                        if "_BUY" in item["operationType"]:
2443                            customStat["buyCount"] += 1
2444
2445                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2446                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2447
2448                            else:
2449                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2450
2451                        # count sell operations:
2452                        elif "_SELL" in item["operationType"]:
2453                            customStat["sellCount"] += 1
2454
2455                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2456                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2457
2458                            else:
2459                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2460
2461                        # count incoming operations:
2462                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2463                            if item["payment"]["currency"] in customStat["payIn"].keys():
2464                                customStat["payIn"][item["payment"]["currency"]] += payment
2465
2466                            else:
2467                                customStat["payIn"][item["payment"]["currency"]] = payment
2468
2469                        # count withdrawals operations:
2470                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2471                            if item["payment"]["currency"] in customStat["payOut"].keys():
2472                                customStat["payOut"][item["payment"]["currency"]] += payment
2473
2474                            else:
2475                                customStat["payOut"][item["payment"]["currency"]] = payment
2476
2477                        # count dividends income:
2478                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2479                            if item["payment"]["currency"] in customStat["divs"].keys():
2480                                customStat["divs"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["divs"][item["payment"]["currency"]] = payment
2484
2485                        # count coupon's income:
2486                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2487                            if item["payment"]["currency"] in customStat["coupons"].keys():
2488                                customStat["coupons"][item["payment"]["currency"]] += payment
2489
2490                            else:
2491                                customStat["coupons"][item["payment"]["currency"]] = payment
2492
2493                        # count broker commissions:
2494                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2495                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2496                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2497
2498                            else:
2499                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2500
2501                        # count service commissions:
2502                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2503                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2504                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2505
2506                            else:
2507                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2508
2509                        # count margin commissions:
2510                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2511                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2512                                customStat["marginCom"][item["payment"]["currency"]] += payment
2513
2514                            else:
2515                                customStat["marginCom"][item["payment"]["currency"]] = payment
2516
2517                        # count withholding taxes:
2518                        elif "_TAX" in item["operationType"]:
2519                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2520                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2521
2522                            else:
2523                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2524
2525                        else:
2526                            continue
2527
2528                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2529
2530                # --- view "Actions" lines:
2531                info.extend([
2532                    "| Report sections            |                               |                              |                      |                        |\n",
2533                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2534                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2535                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2536                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2537                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2538                    ),
2539                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2540                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2541                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2542                    ),
2543                ])
2544
2545                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2546                for key in opsKeys:
2547                    if key == "rub":
2548                        continue
2549
2550                    info.extend([
2551                        "|                            |                               | {:<28} |                      |                        |\n".format(
2552                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2553                        ),
2554                        "|                            |                               | {:<28} |                      |                        |\n".format(
2555                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2556                        ),
2557                    ])
2558
2559                info.append(splitLine1)
2560
2561                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2562                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2563                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2564                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2565                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2566                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2567                    )
2568
2569                # --- view "Payments" lines:
2570                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2571                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2572
2573                for key in paymentsKeys:
2574                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2575
2576                info.append(splitLine1)
2577
2578                # --- view "Commissions and taxes" lines:
2579                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2580                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2581
2582                for key in comKeys:
2583                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2584
2585                info.append(splitLine1)
2586
2587                info.extend([
2588                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2589                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2590                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2591                ])
2592
2593            else:
2594                info.append("Broker returned no operations during this period\n")
2595
2596            # --- view "Operations" section:
2597            for item in ops:
2598                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2599                    continue
2600
2601                else:
2602                    self.figi = item["figi"] if item["figi"] else ""
2603                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2604                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2605
2606                    # group of deals during one day:
2607                    if nextDay and item["date"].split("T")[0] != nextDay:
2608                        info.append(splitLine2)
2609                        nextDay = ""
2610
2611                    else:
2612                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2613
2614                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2615                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2616                        self.figi if self.figi else "—",
2617                        instrument["ticker"] if instrument else "—",
2618                        instrument["type"] if instrument else "—",
2619                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2620                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2621                        TKS_OPERATION_STATES[item["state"]],
2622                        TKS_OPERATION_TYPES[item["operationType"]],
2623                    ))
2624
2625            infoText = "".join(info)
2626
2627            if show:
2628                if self.moreDebug:
2629                    uLogger.debug("Records about history of a client's operations successfully received")
2630
2631                uLogger.info(infoText)
2632
2633            if self.reportFile:
2634                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2635                    fH.write(infoText)
2636
2637                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2638
2639        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2641    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2642        """
2643        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2644
2645        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2646        Warning! Broker server used ISO UTC time by default.
2647
2648        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2649        Also, `historyFile` used to update history with `onlyMissing` parameter.
2650
2651        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2652
2653        :param start: see docstring in `GetDatesAsString()` method.
2654        :param end: see docstring in `GetDatesAsString()` method.
2655        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2656                         `"hour"`, `"day"`. Default: `"hour"`.
2657        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2658                            False by default. Warning! History appends only from last candle to current time
2659                            with always update last candle!
2660        :param csvSep: separator if csv-file is used, `,` by default.
2661        :param show: if `True` then also prints Pandas DataFrame to the console.
2662        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2663                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2664        """
2665        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2666        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2667        history = None  # empty pandas object for history
2668
2669        if interval not in TKS_CANDLE_INTERVALS.keys():
2670            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2671            raise Exception("Incorrect value")
2672
2673        if not (self.ticker or self.figi):
2674            uLogger.error("Ticker or FIGI must be defined!")
2675            raise Exception("Ticker or FIGI required")
2676
2677        if self.ticker and not self.figi:
2678            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2679            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2680
2681        if self.figi and not self.ticker:
2682            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2683            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2684
2685        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2686        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2687        if interval.lower() != "day":
2688            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2689
2690        delta = dtEnd - dtStart  # current UTC time minus last time in file
2691        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2692
2693        # calculate history length in candles:
2694        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2695        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2696            length += 1  # to avoid fraction time
2697
2698        # calculate data blocks count:
2699        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2700
2701        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2702        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2703        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2704        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2705        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2706
2707        tempOld = None  # pandas object for old history, if --only-missing key present
2708        lastTime = None  # datetime object of last old candle in file
2709
2710        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2711            uLogger.debug("--only-missing key present, add only last missing candles...")
2712            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2713
2714            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2715
2716            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2717            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2718            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2719            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2720
2721            # get last datetime object from last string in file or minus 1 delta if file is empty:
2722            if len(tempOld) > 0:
2723                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2724
2725            else:
2726                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2727
2728            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2729
2730        responseJSONs = []  # raw history blocks of data
2731
2732        blockEnd = dtEnd
2733        for item in range(blocks):
2734            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2735            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2736
2737            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2738                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2739            ))
2740
2741            if blockStart == blockEnd:
2742                uLogger.debug("Skipped this zero-length block...")
2743
2744            else:
2745                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2746                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2747                self.body = str({
2748                    "figi": self.figi,
2749                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2750                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2751                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2752                })
2753                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2754
2755                if "code" in responseJSON.keys():
2756                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2757
2758                else:
2759                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2760                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2761
2762                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2763
2764            blockEnd = blockStart
2765
2766        printCount = len(responseJSONs)  # candles to show in console
2767        if responseJSONs:
2768            tempHistory = pd.DataFrame(
2769                data={
2770                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2771                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2772                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2773                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2774                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2775                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2776                    "volume": [int(item["volume"]) for item in responseJSONs],
2777                },
2778                index=range(len(responseJSONs)),
2779                columns=["date", "time", "open", "high", "low", "close", "volume"],
2780            )
2781            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2782            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2783
2784            # append only newest candles to old history if --only-missing key present:
2785            if onlyMissing and tempOld is not None and lastTime is not None:
2786                index = 0  # find start index in tempHistory data:
2787
2788                for i, item in tempHistory.iterrows():
2789                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2790
2791                    if curTime == lastTime:
2792                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2793                        index = i
2794                        printCount = index + 1
2795                        break
2796
2797                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2798
2799            else:
2800                history = tempHistory  # if no `--only-missing` key then load full data from server
2801
2802            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2803
2804        if history is not None and not history.empty:
2805            if show:
2806                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2807                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2808                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2809                ))
2810
2811        else:
2812            uLogger.warning("Received an empty candles history!")
2813
2814        if self.historyFile is not None:
2815            if history is not None and not history.empty:
2816                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2817                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2818
2819            else:
2820                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2821
2822        else:
2823            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2824
2825        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2827    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2828        """
2829        Load candles history from csv-file and return Pandas DataFrame object.
2830
2831        See also: `History()` and `ShowHistoryChart()` methods.
2832
2833        :param filePath: path to csv-file to open.
2834        """
2835        loadedHistory = None  # init candles data object
2836
2837        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2838
2839        if os.path.exists(filePath):
2840            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2841
2842            tfStr = self.priceModel.FormattedDelta(
2843                self.priceModel.timeframe,
2844                "{days} days {hours}h {minutes}m {seconds}s",
2845            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2846                self.priceModel.timeframe,
2847                "{hours}h {minutes}m {seconds}s",
2848            )
2849
2850            if loadedHistory is not None and not loadedHistory.empty:
2851                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2852                    len(loadedHistory),
2853                    tfStr,
2854                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2855                )
2856
2857            else:
2858                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2859
2860        else:
2861            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2862
2863        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2865    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2866        """
2867        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2868
2869        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2870        Default: `index.html` (both for interact and non-interact candlesticks chart).
2871
2872        See also: `History()` and `LoadHistory()` methods.
2873
2874        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2875        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2876                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2877                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2878                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2879        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2880                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2881        """
2882        if isinstance(candles, str):
2883            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2884            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2885
2886        elif isinstance(candles, pd.DataFrame):
2887            self.priceModel.prices = candles  # set candles chain from variable
2888            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2889
2890            if "datetime" not in candles.columns:
2891                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2892
2893        else:
2894            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2895            raise Exception("Incorrect value")
2896
2897        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2898
2899        if interact:
2900            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2901
2902            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2903
2904        else:
2905            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2906
2907            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2908
2909        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2911    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2912        """
2913        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2914        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2915
2916        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2917
2918        :param operation: string "Buy" or "Sell".
2919        :param lots: volume, integer count of lots >= 1.
2920        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2921        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2922        :param expDate: string "Undefined" by default or local date in future,
2923                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2924        :return: JSON with response from broker server.
2925        """
2926        if self.accountId is None or not self.accountId:
2927            uLogger.error("Variable `accountId` must be defined for using this method!")
2928            raise Exception("Account ID required")
2929
2930        if operation is None or not operation or operation not in ("Buy", "Sell"):
2931            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2932            raise Exception("Incorrect value")
2933
2934        if lots is None or lots < 1:
2935            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2936            lots = 1
2937
2938        if tp is None or tp < 0:
2939            tp = 0
2940
2941        if sl is None or sl < 0:
2942            sl = 0
2943
2944        if expDate is None or not expDate:
2945            expDate = "Undefined"
2946
2947        if not (self.ticker or self.figi):
2948            uLogger.error("Ticker or FIGI must be defined!")
2949            raise Exception("Ticker or FIGI required")
2950
2951        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2952        self.ticker = instrument["ticker"]
2953        self.figi = instrument["figi"]
2954
2955        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2956
2957        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2958        self.body = str({
2959            "figi": self.figi,
2960            "quantity": str(lots),
2961            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2962            "accountId": str(self.accountId),
2963            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2964        })
2965        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2966
2967        if "orderId" in response.keys():
2968            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2969                operation, response["orderId"],
2970                self.ticker, self.figi, lots,
2971                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2972                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2973                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2974            ))
2975
2976            if tp > 0:
2977                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2978
2979            if sl > 0:
2980                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2981
2982        else:
2983            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2984
2985        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2987    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2988        """
2989        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2990        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2991
2992        See also: `Order()` and `Trade()` docstrings.
2993
2994        :param lots: volume, integer count of lots >= 1.
2995        :param tp: float > 0, take profit price of stop-order.
2996        :param sl: float > 0, stop loss price of stop-order.
2997        :param expDate: it's a local date in future.
2998                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2999        :return: JSON with response from broker server.
3000        """
3001        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3003    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3004        """
3005        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3006        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3007
3008        See also: `Order()` and `Trade()` docstrings.
3009
3010        :param lots: volume, integer count of lots >= 1.
3011        :param tp: float > 0, take profit price of stop-order.
3012        :param sl: float > 0, stop loss price of stop-order.
3013        :param expDate: it's a local date in the future.
3014                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3015        :return: JSON with response from broker server.
3016        """
3017        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3019    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3020        """
3021        Close position of given instruments.
3022
3023        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3024        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3025                         This avoids unnecessary downloading data from the server.
3026        """
3027        if instruments is None or not instruments:
3028            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3029            raise Exception("Ticker or FIGI required")
3030
3031        if isinstance(instruments, str):
3032            instruments = [instruments]
3033
3034        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3035        if uniqueInstruments:
3036            if portfolio is None or not portfolio:
3037                portfolio = self.Overview(show=False)
3038
3039            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3040            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3041
3042            for self.figi in uniqueInstruments:
3043                if self.figi not in allOpened:
3044                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3045                    continue
3046
3047                # search open trade info about instrument by ticker:
3048                instrument = {}
3049                for iType in TKS_INSTRUMENTS:
3050                    if instrument:
3051                        break
3052
3053                    for item in portfolio["stat"][iType]:
3054                        if item["figi"] == self.figi:
3055                            instrument = item
3056                            break
3057
3058                if instrument:
3059                    self.ticker = instrument["ticker"]
3060                    self.figi = instrument["figi"]
3061
3062                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3063                        self.ticker,
3064                        self.figi,
3065                        int(instrument["volume"]),
3066                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3067                    ))
3068
3069                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3070
3071                    if tradeLots > 0:
3072                        if instrument["blocked"] > 0:
3073                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3074                                instrument["blocked"],
3075                                self.ticker,
3076                                tradeLots,
3077                            ))
3078
3079                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3080                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3081
3082                    else:
3083                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3085    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3086        """
3087        Close all positions of given instruments with defined type.
3088
3089        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3090        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3091                         This avoids unnecessary downloading data from the server.
3092        """
3093        if iType not in TKS_INSTRUMENTS:
3094            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3095
3096        else:
3097            if portfolio is None or not portfolio:
3098                portfolio = self.Overview(show=False)
3099
3100            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3101            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3102
3103            if tickers and portfolio:
3104                self.CloseTrades(tickers, portfolio)
3105
3106            else:
3107                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3109    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3110        """
3111        Universal method to create market or limit orders with all available parameters for current `accountId`.
3112        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3113
3114        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3115        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3116
3117        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3118        then broker immediately open market order as you can do simple --buy or --sell operations!
3119
3120        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3121        When current price will go up or down to target price value then broker opens a limit order.
3122        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3123
3124        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3125
3126        :param operation: string "Buy" or "Sell".
3127        :param orderType: string "Limit" or "Stop".
3128        :param lots: volume, integer count of lots >= 1.
3129        :param targetPrice: target price > 0. This is open trade price for limit order.
3130        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3131                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3132        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3133                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3134                         Stop loss order always executed by market price.
3135        :param expDate: string "Undefined" by default or local date in future.
3136                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3137                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3138                        A limit order has no expiration date, it lasts until the end of the trading day.
3139        :return: JSON with response from broker server.
3140        """
3141        if self.accountId is None or not self.accountId:
3142            uLogger.error("Variable `accountId` must be defined for using this method!")
3143            raise Exception("Account ID required")
3144
3145        if operation is None or not operation or operation not in ("Buy", "Sell"):
3146            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3147            raise Exception("Incorrect value")
3148
3149        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3150            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3151            raise Exception("Incorrect value")
3152
3153        if lots is None or lots < 1:
3154            uLogger.error("You must define trade volume > 0: integer count of lots!")
3155            raise Exception("Incorrect value")
3156
3157        if targetPrice is None or targetPrice <= 0:
3158            uLogger.error("Target price for limit-order must be greater than 0!")
3159            raise Exception("Incorrect value")
3160
3161        if limitPrice is None or limitPrice <= 0:
3162            limitPrice = targetPrice
3163
3164        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3165            stopType = "Limit"
3166
3167        if expDate is None or not expDate:
3168            expDate = "Undefined"
3169
3170        if not (self.ticker or self.figi):
3171            uLogger.error("Tocker or FIGI must be defined!")
3172            raise Exception("Ticker or FIGI required")
3173
3174        response = {}
3175        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3176        self.ticker = instrument["ticker"]
3177        self.figi = instrument["figi"]
3178
3179        if orderType == "Limit":
3180            uLogger.debug(
3181                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3182                    self.ticker, self.figi,
3183                    operation, lots, targetPrice, instrument["currency"],
3184                ))
3185
3186            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3187            self.body = str({
3188                "figi": self.figi,
3189                "quantity": str(lots),
3190                "price": FloatToNano(targetPrice),
3191                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3192                "accountId": str(self.accountId),
3193                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3194            })
3195            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3196
3197            if "orderId" in response.keys():
3198                uLogger.info(
3199                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3200                        response["orderId"],
3201                        self.ticker, self.figi,
3202                        operation, lots, targetPrice, instrument["currency"],
3203                    ))
3204
3205                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3206                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3207                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3208                            targetPrice, instrument["currency"],
3209                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3210                        ))
3211
3212                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3213                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3214                            targetPrice, instrument["currency"],
3215                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3216                        ))
3217
3218            else:
3219                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3220
3221        if orderType == "Stop":
3222            uLogger.debug(
3223                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3224                    self.ticker, self.figi,
3225                    operation, lots,
3226                    targetPrice, instrument["currency"],
3227                    limitPrice, instrument["currency"],
3228                    stopType, expDate,
3229                ))
3230
3231            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3232            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3233            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3234
3235            body = {
3236                "figi": self.figi,
3237                "quantity": str(lots),
3238                "price": FloatToNano(limitPrice),
3239                "stopPrice": FloatToNano(targetPrice),
3240                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3241                "accountId": str(self.accountId),
3242                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3243                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3244            }
3245
3246            if expDateUTC:
3247                body["expireDate"] = expDateUTC
3248
3249            self.body = str(body)
3250            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3251
3252            if "stopOrderId" in response.keys():
3253                uLogger.info(
3254                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3255                        response["stopOrderId"],
3256                        self.ticker, self.figi,
3257                        operation, lots,
3258                        targetPrice, instrument["currency"],
3259                        limitPrice, instrument["currency"],
3260                        TKS_STOP_ORDER_TYPES[stopOrderType],
3261                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3262                    ))
3263
3264                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3265                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3266                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3267                            targetPrice, instrument["currency"],
3268                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3269                        ))
3270
3271                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3272                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3273                            targetPrice, instrument["currency"],
3274                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3275                        ))
3276
3277            else:
3278                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3279
3280        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3282    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3283        """
3284        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3285        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3286        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3287        See also: `Order()` docstring.
3288
3289        :param lots: volume, integer count of lots >= 1.
3290        :param targetPrice: target price > 0. This is open trade price for limit order.
3291        :return: JSON with response from broker server.
3292        """
3293        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3295    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3296        """
3297        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3298        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3299        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3300        target price value then broker opens a limit order. See also: `Order()` docstring.
3301
3302        :param lots: volume, integer count of lots >= 1.
3303        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3304        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3305                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3306        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3307                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3308        :param expDate: string "Undefined" by default or local date in future.
3309                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3310                        This date is converting to UTC format for server.
3311        :return: JSON with response from broker server.
3312        """
3313        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3315    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3316        """
3317        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3318        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3319        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3320        See also: `Order()` docstring.
3321
3322        :param lots: volume, integer count of lots >= 1.
3323        :param targetPrice: target price > 0. This is open trade price for limit order.
3324        :return: JSON with response from broker server.
3325        """
3326        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3328    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3329        """
3330        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3331        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3332        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3333        target price value then broker opens a limit order. See also: `Order()` docstring.
3334
3335        :param lots: volume, integer count of lots >= 1.
3336        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3337        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3338                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3339        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3340                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3341        :param expDate: string "Undefined" by default or local date in future.
3342                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3343                        This date is converting to UTC format for server.
3344        :return: JSON with response from broker server.
3345        """
3346        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3348    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3349        """
3350        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3351
3352        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3353        :param allOrdersIDs: pre-received lists of all active pending orders.
3354                             This avoids unnecessary downloading data from the server.
3355        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3356        """
3357        if self.accountId is None or not self.accountId:
3358            uLogger.error("Variable `accountId` must be defined for using this method!")
3359            raise Exception("Account ID required")
3360
3361        if orderIDs:
3362            if allOrdersIDs is None or not allOrdersIDs:
3363                rawOrders = self.RequestPendingOrders()
3364                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3365
3366            if allStopOrdersIDs is None or not allStopOrdersIDs:
3367                rawStopOrders = self.RequestStopOrders()
3368                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3369
3370            for orderID in orderIDs:
3371                idInPendingOrders = orderID in allOrdersIDs
3372                idInStopOrders = orderID in allStopOrdersIDs
3373
3374                if not (idInPendingOrders or idInStopOrders):
3375                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3376                    continue
3377
3378                else:
3379                    if idInPendingOrders:
3380                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3381
3382                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3383                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3384                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3385                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3386
3387                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3388                            if self.moreDebug:
3389                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3390
3391                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3392
3393                        else:
3394                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3395
3396                    elif idInStopOrders:
3397                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3398
3399                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3400                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3401                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3402                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3403
3404                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3405                            if self.moreDebug:
3406                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3407
3408                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3409
3410                        else:
3411                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3412
3413                    else:
3414                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3416    def CloseAllOrders(self) -> None:
3417        """
3418        Gets a list of open pending and stop orders and cancel it all.
3419        """
3420        rawOrders = self.RequestPendingOrders()
3421        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3422        lenOrders = len(allOrdersIDs)
3423
3424        rawStopOrders = self.RequestStopOrders()
3425        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3426        lenSOrders = len(allStopOrdersIDs)
3427
3428        if lenOrders > 0 or lenSOrders > 0:
3429            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3430
3431            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3432
3433        else:
3434            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3436    def CloseAll(self, *args) -> None:
3437        """
3438        Close all available (not blocked) opened trades and orders.
3439
3440        Also, you can select one or more keywords case-insensitive:
3441        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3442
3443        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3444        """
3445        overview = self.Overview(show=False)  # get all open trades info
3446
3447        if len(args) == 0:
3448            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3449            self.CloseAllOrders()  # close all pending and stop orders
3450
3451            for iType in TKS_INSTRUMENTS:
3452                if iType != "Currencies":
3453                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3454
3455        else:
3456            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3457            lowerArgs = [x.lower() for x in args]
3458
3459            if "orders" in lowerArgs:
3460                self.CloseAllOrders()  # close all pending and stop orders
3461
3462            for iType in TKS_INSTRUMENTS:
3463                if iType.lower() in lowerArgs and iType != "Currencies":
3464                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3466    @staticmethod
3467    def ParseOrderParameters(operation, **inputParameters):
3468        """
3469        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3470
3471        :param operation: string "Buy" or "Sell".
3472        :param inputParameters: this is dict of strings that looks like this
3473               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3474               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3475               "prices" key: one or more prices to open limit-orders
3476               Counts of values in lots and prices lists must be equals!
3477        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3478        """
3479        # TODO: update order grid work with api v2
3480        pass
3481        # uLogger.debug("Input parameters: {}".format(inputParameters))
3482        #
3483        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3484        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3485        #     raise Exception("Incorrect value")
3486        #
3487        # if "l" in inputParameters.keys():
3488        #     inputParameters["lots"] = inputParameters.pop("l")
3489        #
3490        # if "p" in inputParameters.keys():
3491        #     inputParameters["prices"] = inputParameters.pop("p")
3492        #
3493        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3494        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3495        #     raise Exception("Incorrect value")
3496        #
3497        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3498        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3499        #
3500        # if len(lots) != len(prices):
3501        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3502        #     raise Exception("Incorrect value")
3503        #
3504        # uLogger.debug("Extracted parameters for orders:")
3505        # uLogger.debug("lots = {}".format(lots))
3506        # uLogger.debug("prices = {}".format(prices))
3507        #
3508        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3509        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3510        # uLogger.debug("Order parameters: {}".format(result))
3511        #
3512        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3514    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3515        """
3516        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3517
3518        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3519        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3520        """
3521        result = False
3522        msg = "Instrument not defined!"
3523
3524        if portfolio is None or not portfolio:
3525            portfolio = self.Overview(show=False)
3526
3527        if self.ticker:
3528            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3529            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3530
3531            for iType in TKS_INSTRUMENTS:
3532                for instrument in portfolio["stat"][iType]:
3533                    if instrument["ticker"] == self.ticker:
3534                        result = True
3535                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3536                        break
3537
3538        elif self.figi:
3539            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3540            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3541
3542            for iType in TKS_INSTRUMENTS:
3543                for instrument in portfolio["stat"][iType]:
3544                    if instrument["figi"] == self.figi:
3545                        result = True
3546                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3547                        break
3548
3549        else:
3550            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3551
3552        uLogger.debug(msg)
3553
3554        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3556    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3557        """
3558        Returns instrument is in the user's portfolio if it presents there.
3559        Instrument must be defined by `ticker` (highly priority) or `figi`.
3560
3561        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3562        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3563        """
3564        result = None
3565        msg = "Instrument not defined!"
3566
3567        if portfolio is None or not portfolio:
3568            portfolio = self.Overview(show=False)
3569
3570        if self.ticker:
3571            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3572            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3573
3574            for iType in TKS_INSTRUMENTS:
3575                for instrument in portfolio["stat"][iType]:
3576                    if instrument["ticker"] == self.ticker:
3577                        result = instrument
3578                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3579                        break
3580
3581        elif self.figi:
3582            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3583            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3584
3585            for iType in TKS_INSTRUMENTS:
3586                for instrument in portfolio["stat"][iType]:
3587                    if instrument["figi"] == self.figi:
3588                        result = instrument
3589                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3590                        break
3591
3592        else:
3593            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3594
3595        uLogger.debug(msg)
3596
3597        return result

Returns instrument is in the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3599    def RequestLimits(self) -> dict:
3600        """
3601        Method for obtaining the available funds for withdrawal for current `accountId`.
3602
3603        See also:
3604        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3605        - `OverviewLimits()` method
3606
3607        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3608                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3609                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3610                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3611        """
3612        if self.accountId is None or not self.accountId:
3613            uLogger.error("Variable `accountId` must be defined for using this method!")
3614            raise Exception("Account ID required")
3615
3616        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3617
3618        self.body = str({"accountId": self.accountId})
3619        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3620        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3621
3622        if self.moreDebug:
3623            uLogger.debug("Records about available funds for withdrawal successfully received")
3624
3625        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3627    def OverviewLimits(self, show: bool = False) -> dict:
3628        """
3629        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3630
3631        See also: `RequestLimits()`.
3632
3633        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3634        :return: dict with raw parsed data from server and some calculated statistics about it.
3635        """
3636        if self.accountId is None or not self.accountId:
3637            uLogger.error("Variable `accountId` must be defined for using this method!")
3638            raise Exception("Account ID required")
3639
3640        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3641
3642        view = {
3643            "rawLimits": rawLimits,
3644            "limits": {  # parsed data for every currency:
3645                "money": {  # this is an array of portfolio currency positions
3646                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3647                },
3648                "blocked": {  # this is an array of blocked currency
3649                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3650                },
3651                "blockedGuarantee": {  # this is locked money under collateral for futures
3652                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3653                },
3654            },
3655        }
3656
3657        # --- Prepare text table with limits in human-readable format:
3658        if show:
3659            info = [
3660                "# Withdrawal limits\n\n",
3661                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3662                "* **Account ID:** [{}]\n".format(self.accountId),
3663            ]
3664
3665            if view["limits"]["money"]:
3666                info.extend([
3667                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3668                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3669                ])
3670
3671            else:
3672                info.append("\nNo withdrawal limits\n")
3673
3674            for curr in view["limits"]["money"].keys():
3675                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3676                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3677                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3678
3679                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3680                    "[{}]".format(curr),
3681                    "{:.2f}".format(view["limits"]["money"][curr]),
3682                    "{:.2f}".format(availableMoney),
3683                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3684                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3685                )
3686
3687                if curr == "rub":
3688                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3689
3690                else:
3691                    info.append(infoStr)
3692
3693            infoText = "".join(info)
3694
3695            uLogger.info(infoText)
3696
3697            if self.withdrawalLimitsFile:
3698                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3699                    fH.write(infoText)
3700
3701                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3702
3703        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3705    def RequestAccounts(self) -> dict:
3706        """
3707        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3708
3709        See also:
3710        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3711        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3712        - `OverviewUserInfo()` method
3713
3714        :return: dict with raw data from server that contains accounts info. Example of dict:
3715                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3716                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3717                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3718                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3719        """
3720        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3721
3722        self.body = str({})
3723        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3724        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3725
3726        if self.moreDebug:
3727            uLogger.debug("Records about available accounts successfully received")
3728
3729        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3731    def RequestUserInfo(self) -> dict:
3732        """
3733        Method for requesting common user's information.
3734
3735        See also:
3736        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3737        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3738        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3739        - `OverviewUserInfo()` method
3740
3741        :return: dict with raw data from server that contains user's information. Example of dict:
3742                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3743                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3744        """
3745        uLogger.debug("Requesting common user's information. Wait, please...")
3746
3747        self.body = str({})
3748        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3749        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3750
3751        if self.moreDebug:
3752            uLogger.debug("Records about current user successfully received")
3753
3754        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3756    def RequestMarginStatus(self, accountId: str = None) -> dict:
3757        """
3758        Method for requesting margin calculation for defined account ID.
3759
3760        See also:
3761        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3762        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3763        - `OverviewUserInfo()` method
3764
3765        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3766        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3767                 Example of responses:
3768                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3769                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3770                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3771                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3772                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3773                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3774        """
3775        if accountId is None or not accountId:
3776            if self.accountId is None or not self.accountId:
3777                uLogger.error("Variable `accountId` must be defined for using this method!")
3778                raise Exception("Account ID required")
3779
3780            else:
3781                accountId = self.accountId  # use `self.accountId` (main ID) by default
3782
3783        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3784
3785        self.body = str({"accountId": accountId})
3786        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3787        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3788
3789        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3790            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3791            rawMargin = {}
3792
3793        else:
3794            if self.moreDebug:
3795                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3796
3797        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3799    def RequestTariffLimits(self) -> dict:
3800        """
3801        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3802
3803        See also:
3804        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3805        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3806        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3807        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3808        - `OverviewUserInfo()` method
3809
3810        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3811                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3812                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3813        """
3814        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3815
3816        self.body = str({})
3817        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3818        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3819
3820        if self.moreDebug:
3821            uLogger.debug("Records with limits of current tariff successfully received")
3822
3823        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3825    def RequestBondCoupons(self, iJSON: dict) -> dict:
3826        """
3827        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3828        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3829        All dates are in UTC timezone.
3830
3831        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3832        Documentation:
3833        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3834        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3835
3836        See also: `ExtendBondsData()`.
3837
3838        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3839                      If raw iJSON is not data of bond then server returns an error [400] with message:
3840                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3841        :return: dictionary with bond payment calendar. Response example
3842                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3843                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3844                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3845                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3846        """
3847        if iJSON["figi"] is None or not iJSON["figi"]:
3848            uLogger.error("FIGI must be defined for using this method!")
3849            raise Exception("FIGI required")
3850
3851        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3852        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3853
3854        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3855            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3856            self.figi,
3857            startDate,
3858            endDate,
3859        ))
3860
3861        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3862        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3863        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3864
3865        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3866            uLogger.warning("Instrument type is not bond!")
3867
3868        else:
3869            if self.moreDebug:
3870                uLogger.debug("Records about bond payment calendar successfully received")
3871
3872        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3874    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3875        """
3876        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3877        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3878        coupon yields, current yields and some statistics etc.
3879
3880        WARNING! This is too long operation if a lot of bonds requested from broker server.
3881
3882        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3883
3884        :param instruments: list of strings with tickers or FIGIs.
3885        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3886                     for further used by data scientists or stock analytics.
3887        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3888                 In XLSX-file and Pandas DataFrame fields mean:
3889                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3890                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3891        """
3892        if instruments is None or not instruments:
3893            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3894            raise Exception("Ticker or FIGI required")
3895
3896        if isinstance(instruments, str):
3897            instruments = [instruments]
3898
3899        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3900
3901        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3902
3903        iCount = len(uniqueInstruments)
3904        tooLong = iCount >= 20
3905        if tooLong:
3906            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3907
3908        bonds = None
3909        for i, self.figi in enumerate(uniqueInstruments):
3910            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3911
3912            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3913                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3914                rawBond = self.SearchByFIGI(requestPrice=True)
3915
3916                # Widen raw data with UTC current time (iData["actualDateTime"]):
3917                actualDate = datetime.now(tzutc())
3918                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3919
3920                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3921                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3922
3923                # Replace some values with human-readable:
3924                iData["nominalCurrency"] = iData["nominal"]["currency"]
3925                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3926                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3927                iData["aciCurrency"] = iData["aciValue"]["currency"]
3928                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3929                iData["issueSize"] = int(iData["issueSize"])
3930                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3931                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3932                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3933                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3934                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3935                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3936                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3937                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3938                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3939                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3940
3941                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3942                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3943                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3944                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3945                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3946                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3947                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3948                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3949                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3950                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3951                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3952
3953                # Widen raw data with calendar data from `rawCalendar` values:
3954                calendarData = []
3955                if "events" in iData["rawCalendar"].keys():
3956                    for item in iData["rawCalendar"]["events"]:
3957                        calendarData.append({
3958                            "couponDate": item["couponDate"],
3959                            "couponNumber": int(item["couponNumber"]),
3960                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3961                            "payCurrency": item["payOneBond"]["currency"],
3962                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3963                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3964                            "couponStartDate": item["couponStartDate"],
3965                            "couponEndDate": item["couponEndDate"],
3966                            "couponPeriod": item["couponPeriod"],
3967                        })
3968
3969                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3970                    if "maturityDate" not in iData.keys():
3971                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3972
3973                # Widen raw data with Coupon Rate.
3974                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3975                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3976                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3977                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3978
3979                # Widen raw data with Yield to Maturity (YTM) on current date.
3980                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3981                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3982                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3983                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3984                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3985                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3986
3987                iData["calendar"] = calendarData  # adds calendar at the end
3988
3989                # Remove not used data:
3990                iData.pop("uid")
3991                iData.pop("positionUid")
3992                iData.pop("currentPrice")
3993                iData.pop("rawCalendar")
3994
3995                colNames = list(iData.keys())
3996                if bonds is None:
3997                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3998
3999                else:
4000                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4001
4002            else:
4003                uLogger.warning("Instrument is not a bond!")
4004
4005            processed = round(100 * (i + 1) / iCount, 1)
4006            if tooLong and processed % 5 == 0:
4007                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4008
4009            else:
4010                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4011
4012        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4013
4014        # Saving bonds from Pandas DataFrame to XLSX sheet:
4015        if xlsx and self.bondsXLSXFile:
4016            with pd.ExcelWriter(
4017                    path=self.bondsXLSXFile,
4018                    date_format=TKS_DATE_FORMAT,
4019                    datetime_format=TKS_DATE_TIME_FORMAT,
4020                    mode="w",
4021            ) as writer:
4022                bonds.to_excel(
4023                    writer,
4024                    sheet_name="Extended bonds data",
4025                    index=True,
4026                    encoding="UTF-8",
4027                    freeze_panes=(1, 1),
4028                )  # saving as XLSX-file with freeze first row and column as headers
4029
4030            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4031
4032        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4034    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4035        """
4036        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4037
4038        WARNING! This is too long operation if a lot of bonds requested from broker server.
4039
4040        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4041
4042        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4043                        extended information about bonds: main info, current prices, bond payment calendar,
4044                        coupon yields, current yields and some statistics etc.
4045                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4046        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4047                     for further used by data scientists or stock analytics.
4048        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4049        """
4050        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4051            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4052
4053        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4054
4055        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4056        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4057        calendar = None
4058        for bond in extBonds.iterrows():
4059            for item in bond[1]["calendar"]:
4060                cData = {
4061                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4062                    "couponDate": item["couponDate"],
4063                    "figi": bond[1]["figi"],
4064                    "ticker": bond[1]["ticker"],
4065                    "name": bond[1]["name"],
4066                    "couponNumber": item["couponNumber"],
4067                    "payOneBond": item["payOneBond"],
4068                    "payCurrency": item["payCurrency"],
4069                    "couponType": item["couponType"],
4070                    "couponPeriod": item["couponPeriod"],
4071                    "fixDate": item["fixDate"],
4072                    "couponStartDate": item["couponStartDate"],
4073                    "couponEndDate": item["couponEndDate"],
4074                }
4075
4076                if calendar is None:
4077                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4078
4079                else:
4080                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4081
4082        if calendar is not None:
4083            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4084
4085            # Saving calendar from Pandas DataFrame to XLSX sheet:
4086            if xlsx:
4087                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4088
4089                with pd.ExcelWriter(
4090                        path=xlsxCalendarFile,
4091                        date_format=TKS_DATE_FORMAT,
4092                        datetime_format=TKS_DATE_TIME_FORMAT,
4093                        mode="w",
4094                ) as writer:
4095                    humanReadable = calendar.copy(deep=True)
4096                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4097                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4098                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4099                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4100                    humanReadable.columns = colNames  # human-readable column names
4101
4102                    humanReadable.to_excel(
4103                        writer,
4104                        sheet_name="Bond payments calendar",
4105                        index=False,
4106                        encoding="UTF-8",
4107                        freeze_panes=(1, 2),
4108                    )  # saving as XLSX-file with freeze first row and column as headers
4109
4110                    del humanReadable  # release df in memory
4111
4112                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4113
4114        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4116    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4117        """
4118        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4119        Also, creates Markdown file with calendar data, `calendar.md` by default.
4120
4121        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4122
4123        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4124                        extended information about bonds: main info, current prices, bond payment calendar,
4125                        coupon yields, current yields and some statistics etc.
4126                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4127        :param show: if `True` then also printing bonds payment calendar to the console,
4128                     otherwise save to file `calendarFile` only. `False` by default.
4129        :return: multilines text in Markdown format with bonds payment calendar as a table.
4130        """
4131        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4132            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4133
4134        infoText = "# Bond payments calendar\n\n"
4135
4136        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4137
4138        if not (calendar is None or calendar.empty):
4139            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4140
4141            info = [
4142                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4143                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4144            ]
4145
4146            newMonth = False
4147            notOneBond = calendar["figi"].nunique() > 1
4148            for i, bond in enumerate(calendar.iterrows()):
4149                if newMonth and notOneBond:
4150                    info.append(splitLine)
4151
4152                info.append(
4153                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4154                        "  √" if bond[1]["paid"] else "  —",
4155                        bond[1]["couponDate"].split("T")[0],
4156                        bond[1]["figi"],
4157                        bond[1]["ticker"],
4158                        bond[1]["couponNumber"],
4159                        "{} {}".format(
4160                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4161                            bond[1]["payCurrency"],
4162                        ),
4163                        bond[1]["couponType"],
4164                        bond[1]["couponPeriod"],
4165                        bond[1]["fixDate"].split("T")[0],
4166                    )
4167                )
4168
4169                if i < len(calendar.values) - 1:
4170                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4171                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4172                    newMonth = False if curDate.month == nextDate.month else True
4173
4174                else:
4175                    newMonth = False
4176
4177            infoText += "".join(info)
4178
4179            if show:
4180                uLogger.info("{}".format(infoText))
4181
4182            if self.calendarFile is not None:
4183                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4184                    fH.write(infoText)
4185
4186                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4187
4188        else:
4189            infoText += "No data\n"
4190
4191        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4193    def OverviewAccounts(self, show: bool = False) -> dict:
4194        """
4195        Method for parsing and show simple table with all available user accounts.
4196
4197        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4198
4199        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4200        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4201                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4202                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4203                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4204                                                        "closed": "—", "access": "Full access" }, ...}}`
4205        """
4206        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4207
4208        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4209        accounts = {
4210            item["id"]: {
4211                "type": TKS_ACCOUNT_TYPES[item["type"]],
4212                "name": item["name"],
4213                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4214                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4215                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4216                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4217            } for item in rawAccounts["accounts"]
4218        }
4219
4220        # Raw and parsed data with some fields replaced in "stat" section:
4221        view = {
4222            "rawAccounts": rawAccounts,
4223            "stat": accounts,
4224        }
4225
4226        # --- Prepare simple text table with only accounts data in human-readable format:
4227        if show:
4228            info = [
4229                "# User accounts\n\n",
4230                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4231                "| Account ID   | Type                      | Status                    | Name                           |\n",
4232                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4233            ]
4234
4235            for account in view["stat"].keys():
4236                info.extend([
4237                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4238                        account,
4239                        view["stat"][account]["type"],
4240                        view["stat"][account]["status"],
4241                        view["stat"][account]["name"],
4242                    )
4243                ])
4244
4245            infoText = "".join(info)
4246
4247            uLogger.info(infoText)
4248
4249            if self.userAccountsFile:
4250                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4251                    fH.write(infoText)
4252
4253                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4254
4255        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4257    def OverviewUserInfo(self, show: bool = False) -> dict:
4258        """
4259        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4260
4261        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4262
4263        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4264        :return: dict with raw parsed data from server and some calculated statistics about it.
4265        """
4266        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4267        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4268        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4269        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4270        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4271        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4272
4273        # This is dict with parsed common user data:
4274        userInfo = {
4275            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4276            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4277            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4278            "tariff": rawUserInfo["tariff"],
4279        }
4280
4281        # This is an array of dict with parsed margin statuses for every account IDs:
4282        margins = {}
4283        for accountId in accounts.keys():
4284            if rawMargins[accountId]:
4285                margins[accountId] = {
4286                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4287                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4288                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4289                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4290                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4291                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4292                }
4293
4294            else:
4295                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4296
4297        unary = {}  # unary-connection limits
4298        for item in rawTariffLimits["unaryLimits"]:
4299            if item["limitPerMinute"] in unary.keys():
4300                unary[item["limitPerMinute"]].extend(item["methods"])
4301
4302            else:
4303                unary[item["limitPerMinute"]] = item["methods"]
4304
4305        stream = {}  # stream-connection limits
4306        for item in rawTariffLimits["streamLimits"]:
4307            if item["limit"] in stream.keys():
4308                stream[item["limit"]].extend(item["streams"])
4309
4310            else:
4311                stream[item["limit"]] = item["streams"]
4312
4313        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4314        limits = {
4315            "unary": unary,
4316            "stream": stream,
4317        }
4318
4319        # Raw and parsed data as an output result:
4320        view = {
4321            "rawUserInfo": rawUserInfo,
4322            "rawAccounts": rawAccounts,
4323            "rawMargins": rawMargins,
4324            "rawTariffLimits": rawTariffLimits,
4325            "stat": {
4326                "userInfo": userInfo,
4327                "accounts": accounts,
4328                "margins": margins,
4329                "limits": limits,
4330            },
4331        }
4332
4333        # --- Prepare text table with user information in human-readable format:
4334        if show:
4335            info = [
4336                "# Full user information\n\n",
4337                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4338                "## Common information\n\n",
4339                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4340                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4341                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4342                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4343                "\n## User accounts\n\n",
4344            ]
4345
4346            for account in view["stat"]["accounts"].keys():
4347                info.extend([
4348                    "### ID: [{}]\n\n".format(account),
4349                    "| Parameters           | Values                                                       |\n",
4350                    "|----------------------|--------------------------------------------------------------|\n",
4351                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4352                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4353                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4354                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4355                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4356                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4357                ])
4358
4359                if margins[account]:
4360                    info.extend([
4361                        "| Margin status:       | Enabled                                                      |\n",
4362                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4363                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4364                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4365                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4366                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4367                    ])
4368
4369                else:
4370                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4371
4372            info.extend([
4373                "\n## Current user tariff limits\n",
4374                "\nSee also:\n",
4375                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4376                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4377                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4378                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4379                "\n### Unary limits\n",
4380            ])
4381
4382            if unary:
4383                for key, values in sorted(unary.items()):
4384                    info.append("\n* Max requests per minute: {}\n".format(key))
4385
4386                    for value in values:
4387                        info.append("  - {}\n".format(value))
4388
4389            else:
4390                info.append("\nNot available\n")
4391
4392            info.append("\n### Stream limits\n")
4393
4394            if stream:
4395                for key, values in sorted(stream.items()):
4396                    info.append("\n* Max stream connections: {}\n".format(key))
4397
4398                    for value in values:
4399                        info.append("  - {}\n".format(value))
4400
4401            else:
4402                info.append("\nNot available\n")
4403
4404            infoText = "".join(info)
4405
4406            uLogger.info(infoText)
4407
4408            if self.userInfoFile:
4409                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4410                    fH.write(infoText)
4411
4412                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4413
4414        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4417class Args:
4418    """
4419    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4420    """
4421    def __init__(self, **kwargs):
4422        self.__dict__.update(kwargs)
4423
4424    def __getattr__(self, item):
4425        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4421    def __init__(self, **kwargs):
4422        self.__dict__.update(kwargs)
def ParseArgs()
4428def ParseArgs():
4429    """This function get and parse command line keys."""
4430    parser = ArgumentParser()  # command-line string parser
4431
4432    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4433    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4434
4435    # --- options:
4436
4437    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4438    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4439    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4440
4441    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4442    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4443
4444    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4445    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4446
4447    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4448
4449    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4450    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4451    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4452
4453    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4454    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4455
4456    # --- commands:
4457
4458    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4459
4460    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4461    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4462    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4463    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4464    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4465    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4466    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4467    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4468
4469    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4470    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4471    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4472    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4473    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4474
4475    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4476    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4477    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4478    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4479
4480    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4481    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4482    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4483
4484    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4485    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4486    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4487    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4488    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4489    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4490    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4491
4492    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4493    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4494    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4495    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4496    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4497
4498    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4499    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4500    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4501
4502    cmdArgs = parser.parse_args()
4503    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4506def Main(**kwargs):
4507    """
4508    Main function for work with TKSBrokerAPI in the console.
4509
4510    See examples:
4511    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4512    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4513    """
4514    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4515
4516    if args.debug_level:
4517        uLogger.level = 10  # always debug level by default
4518        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4519
4520    exitCode = 0
4521    start = datetime.now(tzutc())
4522    uLogger.debug("=-" * 50)
4523    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4524        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4525        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4526    ))
4527
4528    # trying to calculate full current version:
4529    buildVersion = __version__
4530    try:
4531        v = version("tksbrokerapi")
4532        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4533
4534    except Exception:
4535        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4536
4537    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4538    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4539
4540    try:
4541        if args.version:
4542            print("TKSBrokerAPI {}".format(buildVersion))
4543            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4544
4545        else:
4546            # Init class for trading with Tinkoff Broker:
4547            trader = TinkoffBrokerServer(
4548                token=args.token,
4549                accountId=args.account_id,
4550                useCache=not args.no_cache,
4551            )
4552
4553            # --- set some options:
4554
4555            if args.more:
4556                trader.moreDebug = True
4557                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4558
4559            if args.ticker:
4560                if args.ticker in trader.aliasesKeys:
4561                    trader.ticker = trader.aliases[args.ticker]  # Replace some tickers with its aliases
4562
4563                else:
4564                    trader.ticker = args.ticker
4565
4566            if args.figi:
4567                trader.figi = args.figi
4568
4569            if args.depth is not None:
4570                trader.depth = args.depth
4571
4572            # --- do one command:
4573
4574            if args.list:
4575                if args.output is not None:
4576                    trader.instrumentsFile = args.output
4577
4578                trader.ShowInstrumentsInfo(show=True)
4579
4580            elif args.list_xlsx:
4581                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4582
4583            elif args.bonds_xlsx is not None:
4584                if args.output is not None:
4585                    trader.bondsXLSXFile = args.output
4586
4587                if len(args.bonds_xlsx) == 0:
4588                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4589
4590                else:
4591                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4592
4593            elif args.search:
4594                if args.output is not None:
4595                    trader.searchResultsFile = args.output
4596
4597                trader.SearchInstruments(pattern=args.search[0], show=True)
4598
4599            elif args.info:
4600                if not (args.ticker or args.figi):
4601                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4602                    raise Exception("Ticker or FIGI required")
4603
4604                if args.output is not None:
4605                    trader.infoFile = args.output
4606
4607                if args.ticker:
4608                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4609
4610                else:
4611                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4612
4613            elif args.calendar is not None:
4614                if args.output is not None:
4615                    trader.calendarFile = args.output
4616
4617                if len(args.calendar) == 0:
4618                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4619
4620                else:
4621                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4622
4623                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4624
4625            elif args.price:
4626                if not (args.ticker or args.figi):
4627                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4628                    raise Exception("Ticker or FIGI required")
4629
4630                trader.GetCurrentPrices(show=True)
4631
4632            elif args.prices is not None:
4633                if args.output is not None:
4634                    trader.pricesFile = args.output
4635
4636                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4637
4638            elif args.overview:
4639                if args.output is not None:
4640                    trader.overviewFile = args.output
4641
4642                trader.Overview(show=True, details="full")
4643
4644            elif args.overview_digest:
4645                if args.output is not None:
4646                    trader.overviewDigestFile = args.output
4647
4648                trader.Overview(show=True, details="digest")
4649
4650            elif args.overview_positions:
4651                if args.output is not None:
4652                    trader.overviewPositionsFile = args.output
4653
4654                trader.Overview(show=True, details="positions")
4655
4656            elif args.overview_orders:
4657                if args.output is not None:
4658                    trader.overviewOrdersFile = args.output
4659
4660                trader.Overview(show=True, details="orders")
4661
4662            elif args.overview_analytics:
4663                if args.output is not None:
4664                    trader.overviewAnalyticsFile = args.output
4665
4666                trader.Overview(show=True, details="analytics")
4667
4668            elif args.deals is not None:
4669                if args.output is not None:
4670                    trader.reportFile = args.output
4671
4672                if 0 <= len(args.deals) < 3:
4673                    trader.Deals(
4674                        start=args.deals[0] if len(args.deals) >= 1 else None,
4675                        end=args.deals[1] if len(args.deals) == 2 else None,
4676                        show=True,  # Always show deals report in console
4677                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4678                    )
4679
4680                else:
4681                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4682                    raise Exception("Incorrect value")
4683
4684            elif args.history is not None:
4685                if args.output is not None:
4686                    trader.historyFile = args.output
4687
4688                if 0 <= len(args.history) < 3:
4689                    dataReceived = trader.History(
4690                        start=args.history[0] if len(args.history) >= 1 else None,
4691                        end=args.history[1] if len(args.history) == 2 else None,
4692                        interval="hour" if args.interval is None or not args.interval else args.interval,
4693                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4694                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4695                        show=True,  # shows all downloaded candles in console
4696                    )
4697
4698                    if args.render_chart is not None and dataReceived is not None:
4699                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4700
4701                        trader.ShowHistoryChart(
4702                            candles=dataReceived,
4703                            interact=iChart,
4704                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4705                        )
4706
4707                else:
4708                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4709                    raise Exception("Incorrect value")
4710
4711            elif args.load_history is not None:
4712                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4713
4714                if args.render_chart is not None and histData is not None:
4715                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4716                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4717
4718                    trader.ShowHistoryChart(
4719                        candles=histData,
4720                        interact=iChart,
4721                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4722                    )
4723
4724            elif args.trade is not None:
4725                if 1 <= len(args.trade) <= 5:
4726                    trader.Trade(
4727                        operation=args.trade[0],
4728                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4729                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4730                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4731                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4732                    )
4733
4734                else:
4735                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4736
4737            elif args.buy is not None:
4738                if 0 <= len(args.buy) <= 4:
4739                    trader.Buy(
4740                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4741                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4742                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4743                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4744                    )
4745
4746                else:
4747                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4748
4749            elif args.sell is not None:
4750                if 0 <= len(args.sell) <= 4:
4751                    trader.Sell(
4752                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4753                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4754                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4755                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4756                    )
4757
4758                else:
4759                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4760
4761            elif args.order:
4762                if 4 <= len(args.order) <= 7:
4763                    trader.Order(
4764                        operation=args.order[0],
4765                        orderType=args.order[1],
4766                        lots=int(args.order[2]),
4767                        targetPrice=float(args.order[3]),
4768                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4769                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4770                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4771                    )
4772
4773                else:
4774                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4775
4776            elif args.buy_limit:
4777                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4778
4779            elif args.sell_limit:
4780                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4781
4782            elif args.buy_stop:
4783                if 2 <= len(args.buy_stop) <= 7:
4784                    trader.BuyStop(
4785                        lots=int(args.buy_stop[0]),
4786                        targetPrice=float(args.buy_stop[1]),
4787                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4788                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4789                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4790                    )
4791
4792                else:
4793                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4794
4795            elif args.sell_stop:
4796                if 2 <= len(args.sell_stop) <= 7:
4797                    trader.SellStop(
4798                        lots=int(args.sell_stop[0]),
4799                        targetPrice=float(args.sell_stop[1]),
4800                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4801                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4802                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4803                    )
4804
4805                else:
4806                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4807
4808            # elif args.buy_order_grid is not None:
4809            #     # update order grid work with api v2
4810            #     if len(args.buy_order_grid) == 2:
4811            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4812            #
4813            #         for order in orderParams:
4814            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4815            #
4816            #     else:
4817            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4818            #
4819            # elif args.sell_order_grid is not None:
4820            #     # update order grid work with api v2
4821            #     if len(args.sell_order_grid) >= 2:
4822            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4823            #
4824            #         for order in orderParams:
4825            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4826            #
4827            #     else:
4828            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4829
4830            elif args.close_order is not None:
4831                trader.CloseOrders(args.close_order)  # close only one order
4832
4833            elif args.close_orders is not None:
4834                trader.CloseOrders(args.close_orders)  # close list of orders
4835
4836            elif args.close_trade:
4837                if not (args.ticker or args.figi):
4838                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4839                    raise Exception("Ticker or FIGI required")
4840
4841                if args.ticker:
4842                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4843
4844                else:
4845                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4846
4847            elif args.close_trades is not None:
4848                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4849
4850            elif args.close_all is not None:
4851                trader.CloseAll(*args.close_all)
4852
4853            elif args.limits:
4854                if args.output is not None:
4855                    trader.withdrawalLimitsFile = args.output
4856
4857                trader.OverviewLimits(show=True)
4858
4859            elif args.user_info:
4860                if args.output is not None:
4861                    trader.userInfoFile = args.output
4862
4863                trader.OverviewUserInfo(show=True)
4864
4865            elif args.account:
4866                if args.output is not None:
4867                    trader.userAccountsFile = args.output
4868
4869                trader.OverviewAccounts(show=True)
4870
4871            else:
4872                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4873                raise Exception("There is no command to execute")
4874
4875    except Exception:
4876        trace = tb.format_exc()
4877        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4878            if e in trace:
4879                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4880                break
4881
4882        uLogger.debug(trace)
4883        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4884        exitCode = 255  # an error occurred, must be open a ticket for this issue
4885
4886    finally:
4887        finish = datetime.now(tzutc())
4888
4889        if exitCode == 0:
4890            if args.more:
4891                uLogger.debug("All operations were finished success (summary code is 0).")
4892
4893        else:
4894            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4895                os.path.abspath(uLog.defaultLogFile), exitCode,
4896            ))
4897
4898        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4899        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4900            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4901            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4902        ))
4903        uLogger.debug("=-" * 50)
4904
4905        if not kwargs:
4906            sys.exit(exitCode)
4907
4908        else:
4909            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: